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.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.designhub.common.Result;
|
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.service.RewardRuleService;
|
||||||
import com.designhub.util.JwtUtil;
|
import com.designhub.util.JwtUtil;
|
||||||
|
import com.designhub.vo.RewardStatsVO;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,6 +26,7 @@ import java.util.List;
|
|||||||
public class RewardRuleController {
|
public class RewardRuleController {
|
||||||
|
|
||||||
private final RewardRuleService rewardRuleService;
|
private final RewardRuleService rewardRuleService;
|
||||||
|
private final RewardRecordService rewardRecordService;
|
||||||
private final JwtUtil jwtUtil;
|
private final JwtUtil jwtUtil;
|
||||||
|
|
||||||
@Operation(summary = "创建奖励规则")
|
@Operation(summary = "创建奖励规则")
|
||||||
@ -89,4 +94,101 @@ public class RewardRuleController {
|
|||||||
rewardRuleService.initDefaultRules();
|
rewardRuleService.initDefaultRules();
|
||||||
return Result.success();
|
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.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.designhub.common.ResultCode;
|
import com.designhub.common.ResultCode;
|
||||||
|
import com.designhub.entity.RewardRecord;
|
||||||
import com.designhub.entity.RewardRule;
|
import com.designhub.entity.RewardRule;
|
||||||
import com.designhub.entity.Task;
|
import com.designhub.entity.Task;
|
||||||
import com.designhub.entity.User;
|
import com.designhub.entity.User;
|
||||||
import com.designhub.exception.BusinessException;
|
import com.designhub.exception.BusinessException;
|
||||||
|
import com.designhub.mapper.RewardRecordMapper;
|
||||||
import com.designhub.mapper.RewardRuleMapper;
|
import com.designhub.mapper.RewardRuleMapper;
|
||||||
import com.designhub.mapper.TaskMapper;
|
import com.designhub.mapper.TaskMapper;
|
||||||
import com.designhub.mapper.UserMapper;
|
import com.designhub.mapper.UserMapper;
|
||||||
@ -34,6 +36,7 @@ import java.util.Map;
|
|||||||
public class RewardRuleService {
|
public class RewardRuleService {
|
||||||
|
|
||||||
private final RewardRuleMapper rewardRuleMapper;
|
private final RewardRuleMapper rewardRuleMapper;
|
||||||
|
private final RewardRecordMapper rewardRecordMapper;
|
||||||
private final UserMapper userMapper;
|
private final UserMapper userMapper;
|
||||||
private final TaskMapper taskMapper;
|
private final TaskMapper taskMapper;
|
||||||
private final NotificationService notificationService;
|
private final NotificationService notificationService;
|
||||||
@ -240,11 +243,43 @@ public class RewardRuleService {
|
|||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void applyReward(RewardRule rule, User user) {
|
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) {
|
if (rule.getRewardScore() != null && rule.getRewardScore() > 0) {
|
||||||
int currentScore = user.getScore() != null ? user.getScore() : 0;
|
int currentScore = user.getScore() != null ? user.getScore() : 0;
|
||||||
user.setScore(currentScore + rule.getRewardScore());
|
user.setScore(currentScore + rule.getRewardScore());
|
||||||
userMapper.updateById(user);
|
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(
|
notificationService.sendNotification(
|
||||||
user.getId(),
|
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
|
cronExpression?: string
|
||||||
lastExecuteTime?: string
|
lastExecuteTime?: string
|
||||||
priority: number
|
priority: number
|
||||||
|
triggerCount?: number
|
||||||
|
totalBudget?: number
|
||||||
|
usedBudget?: number
|
||||||
|
maxTimesPerUser?: number
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
creatorId: number
|
creatorId: number
|
||||||
createTime: string
|
createTime: string
|
||||||
updateTime: 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 {
|
export interface RewardRuleCreateRequest {
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
@ -116,6 +201,115 @@ export function initDefaultRewardRules(): Promise<void> {
|
|||||||
.then(res => res.data)
|
.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 = [
|
export const RULE_TYPE_OPTIONS = [
|
||||||
{ value: 'daily', label: '每日奖励' },
|
{ value: 'daily', label: '每日奖励' },
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1 class="page-title">
|
<h1 class="page-title">
|
||||||
<Icon name="settings" size="xl" color="#31f08b" style="margin-right: 12px;" />
|
<Icon name="settings" size="xl" color="#31f08b" style="margin-right: 12px;" />
|
||||||
自动奖励规则
|
奖励规则管理
|
||||||
</h1>
|
</h1>
|
||||||
<p class="page-desc">配置系统自动奖励规则,激励团队成长</p>
|
<p class="page-desc">配置自动奖励规则,查看奖励发放统计</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<a-button type="primary" @click="showCreateModal">
|
<a-button type="primary" @click="showCreateModal">
|
||||||
@ -19,88 +19,223 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 筛选 -->
|
<!-- Tab切换 -->
|
||||||
<div class="filter-bar">
|
<a-tabs v-model:activeKey="activeTab" class="reward-tabs">
|
||||||
<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">
|
<a-tab-pane key="rules" tab="规则管理">
|
||||||
{{ opt.label }}
|
<!-- 筛选 -->
|
||||||
</a-select-option>
|
<div class="filter-bar">
|
||||||
</a-select>
|
<a-space>
|
||||||
</div>
|
<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 class="rules-list">
|
||||||
<div v-if="loading" class="loading-container">
|
<div v-if="loading" class="loading-container">
|
||||||
<a-spin size="large" />
|
<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>
|
</div>
|
||||||
|
<div v-else-if="rules.length === 0" class="empty-state">
|
||||||
<p class="rule-desc">{{ rule.description }}</p>
|
<Icon name="inbox" size="xl" color="#6B6B6B" />
|
||||||
|
<p>暂无奖励规则</p>
|
||||||
<div class="rule-details">
|
<a-button type="primary" @click="showCreateModal">创建规则</a-button>
|
||||||
<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>
|
</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">
|
<div class="rule-footer">
|
||||||
<span class="rule-time">创建于 {{ formatDate(rule.createTime) }}</span>
|
<span class="rule-time">创建于 {{ formatDate(rule.createTime) }}</span>
|
||||||
<div class="rule-actions">
|
<div class="rule-actions">
|
||||||
<a-button size="small" @click="editRule(rule)">编辑</a-button>
|
<a-button size="small" @click="editRule(rule)">编辑</a-button>
|
||||||
<a-popconfirm title="确定删除此规则吗?" @confirm="handleDelete(rule.id)">
|
<a-popconfirm title="确定删除此规则吗?" @confirm="handleDelete(rule.id)">
|
||||||
<a-button size="small" danger>删除</a-button>
|
<a-button size="small" danger>删除</a-button>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container" v-if="total > 0">
|
<div class="pagination-container" v-if="total > 0">
|
||||||
<a-pagination
|
<a-pagination
|
||||||
v-model:current="currentPage"
|
v-model:current="currentPage"
|
||||||
:total="total"
|
:total="total"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
show-size-changer
|
show-size-changer
|
||||||
@change="handlePageChange"
|
@change="handlePageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -184,7 +319,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||||||
import Icon from '@/components/Icon.vue'
|
import Icon from '@/components/Icon.vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
@ -196,24 +331,39 @@ import {
|
|||||||
deleteRewardRule,
|
deleteRewardRule,
|
||||||
enableRewardRule,
|
enableRewardRule,
|
||||||
disableRewardRule,
|
disableRewardRule,
|
||||||
|
getRewardStats,
|
||||||
|
getRewardRecords,
|
||||||
|
getAllBadges,
|
||||||
|
getAllTitles,
|
||||||
RULE_TYPE_OPTIONS,
|
RULE_TYPE_OPTIONS,
|
||||||
TRIGGER_TYPE_OPTIONS,
|
TRIGGER_TYPE_OPTIONS,
|
||||||
OPERATOR_OPTIONS,
|
OPERATOR_OPTIONS,
|
||||||
type RewardRule
|
type RewardRule,
|
||||||
|
type RewardStats,
|
||||||
|
type RewardRecord,
|
||||||
|
type BadgeDefinition,
|
||||||
|
type TitleDefinition
|
||||||
} from '@/api/rewardRule'
|
} from '@/api/rewardRule'
|
||||||
|
|
||||||
|
// Tab状态
|
||||||
|
const activeTab = ref('rules')
|
||||||
|
|
||||||
|
// 规则列表状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rules = ref<RewardRule[]>([])
|
const rules = ref<RewardRule[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
const filterType = ref<string | undefined>(undefined)
|
const filterType = ref<string | undefined>(undefined)
|
||||||
|
const filterStatus = ref<string | undefined>(undefined)
|
||||||
|
|
||||||
|
// 弹窗状态
|
||||||
const modalVisible = ref(false)
|
const modalVisible = ref(false)
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const editingId = ref<number | null>(null)
|
const editingId = ref<number | null>(null)
|
||||||
|
|
||||||
|
// 规则表单
|
||||||
const ruleForm = ref({
|
const ruleForm = ref({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
@ -226,6 +376,29 @@ const ruleForm = ref({
|
|||||||
priority: 100
|
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 () => {
|
const loadData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
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 = () => {
|
const showCreateModal = () => {
|
||||||
isEdit.value = false
|
isEdit.value = false
|
||||||
editingId.value = null
|
editingId.value = null
|
||||||
@ -382,7 +605,11 @@ const getScopeName = (scope: string) => {
|
|||||||
|
|
||||||
const formatCondition = (condition: any) => {
|
const formatCondition = (condition: any) => {
|
||||||
if (typeof condition === 'string') {
|
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 typeOpt = TRIGGER_TYPE_OPTIONS.find(o => o.value === condition.type)
|
||||||
const opOpt = OPERATOR_OPTIONS.find(o => o.value === condition.operator)
|
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 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(() => {
|
onMounted(() => {
|
||||||
loadData()
|
loadData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@import "@/styles/variables.scss";
|
||||||
|
|
||||||
.reward-rules-page {
|
.reward-rules-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
background: $bg-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1000px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,14 +657,14 @@ onMounted(() => {
|
|||||||
.page-title {
|
.page-title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #ECECEC;
|
color: $text-primary;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-desc {
|
.page-desc {
|
||||||
color: #AFAFAF;
|
color: $text-secondary;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,10 +679,15 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rule-card {
|
.rule-card {
|
||||||
background: #2A2A2A;
|
background: $bg-card;
|
||||||
border: 1px solid #3A3A3A;
|
border: 1px solid $border-primary;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $brand-primary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-header {
|
.rule-header {
|
||||||
@ -461,11 +706,11 @@ onMounted(() => {
|
|||||||
.rule-name {
|
.rule-name {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #ECECEC;
|
color: $text-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-desc {
|
.rule-desc {
|
||||||
color: #AFAFAF;
|
color: $text-secondary;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@ -476,7 +721,7 @@ onMounted(() => {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #333;
|
background: $bg-secondary;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,15 +733,15 @@ onMounted(() => {
|
|||||||
|
|
||||||
.detail-label {
|
.detail-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #6B6B6B;
|
color: $text-tertiary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-value {
|
.detail-value {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #ECECEC;
|
color: $text-primary;
|
||||||
|
|
||||||
&.reward {
|
&.reward {
|
||||||
color: #31f08b;
|
color: $brand-primary;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -506,12 +751,12 @@ onMounted(() => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
border-top: 1px solid #3A3A3A;
|
border-top: 1px solid $border-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-time {
|
.rule-time {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #6B6B6B;
|
color: $text-tertiary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-actions {
|
.rule-actions {
|
||||||
@ -528,7 +773,7 @@ onMounted(() => {
|
|||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 48px;
|
padding: 48px;
|
||||||
color: #6B6B6B;
|
color: $text-tertiary;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
@ -540,4 +785,206 @@ onMounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 48px;
|
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>
|
</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