chore: initial commit with CI pipeline, review and tasks docs

This commit is contained in:
Your Name
2025-09-30 16:39:51 +08:00
commit 8a7afc8a00
76 changed files with 5091 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
package com.mosquito.project;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@EnableCaching
public class MosquitoApplication {
public static void main(String[] args) {
SpringApplication.run(MosquitoApplication.class, args);
}
}

View File

@@ -0,0 +1,59 @@
package com.mosquito.project.controller;
import com.mosquito.project.domain.Activity;
import com.mosquito.project.dto.CreateActivityRequest;
import com.mosquito.project.dto.UpdateActivityRequest;
import com.mosquito.project.dto.ActivityStatsResponse;
import com.mosquito.project.dto.ActivityGraphResponse;
import com.mosquito.project.service.ActivityService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/activities")
public class ActivityController {
private final ActivityService activityService;
public ActivityController(ActivityService activityService) {
this.activityService = activityService;
}
@PostMapping
public ResponseEntity<Activity> createActivity(@Valid @RequestBody CreateActivityRequest request) {
Activity createdActivity = activityService.createActivity(request);
return new ResponseEntity<>(createdActivity, HttpStatus.CREATED);
}
@PutMapping("/{id}")
public ResponseEntity<Activity> updateActivity(@PathVariable Long id, @Valid @RequestBody UpdateActivityRequest request) {
Activity updatedActivity = activityService.updateActivity(id, request);
return ResponseEntity.ok(updatedActivity);
}
@GetMapping("/{id}")
public ResponseEntity<Activity> getActivityById(@PathVariable Long id) {
Activity activity = activityService.getActivityById(id);
return ResponseEntity.ok(activity);
}
@GetMapping("/{id}/stats")
public ResponseEntity<ActivityStatsResponse> getActivityStats(@PathVariable Long id) {
ActivityStatsResponse stats = activityService.getActivityStats(id);
return ResponseEntity.ok(stats);
}
@GetMapping("/{id}/graph")
public ResponseEntity<ActivityGraphResponse> getActivityGraph(@PathVariable Long id) {
ActivityGraphResponse graph = activityService.getActivityGraph(id);
return ResponseEntity.ok(graph);
}
}

View File

@@ -0,0 +1,37 @@
package com.mosquito.project.controller;
import com.mosquito.project.dto.CreateApiKeyRequest;
import com.mosquito.project.dto.CreateApiKeyResponse;
import com.mosquito.project.service.ActivityService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/api-keys")
public class ApiKeyController {
private final ActivityService activityService;
public ApiKeyController(ActivityService activityService) {
this.activityService = activityService;
}
@PostMapping
public ResponseEntity<CreateApiKeyResponse> createApiKey(@Valid @RequestBody CreateApiKeyRequest request) {
String rawApiKey = activityService.generateApiKey(request);
return new ResponseEntity<>(new CreateApiKeyResponse(rawApiKey), HttpStatus.CREATED);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> revokeApiKey(@PathVariable Long id) {
activityService.revokeApiKey(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

View File

@@ -0,0 +1,81 @@
package com.mosquito.project.domain;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Set;
public class Activity {
private Long id;
private String name;
private ZonedDateTime startTime;
private ZonedDateTime endTime;
private Set<Long> targetUserIds;
private List<RewardTier> rewardTiers;
private RewardMode rewardMode = RewardMode.DIFFERENTIAL; // 默认为补差模式
private List<MultiLevelRewardRule> multiLevelRewardRules;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ZonedDateTime getStartTime() {
return startTime;
}
public void setStartTime(ZonedDateTime startTime) {
this.startTime = startTime;
}
public ZonedDateTime getEndTime() {
return endTime;
}
public void setEndTime(ZonedDateTime endTime) {
this.endTime = endTime;
}
public Set<Long> getTargetUserIds() {
return targetUserIds;
}
public void setTargetUserIds(Set<Long> targetUserIds) {
this.targetUserIds = targetUserIds;
}
public List<RewardTier> getRewardTiers() {
return rewardTiers;
}
public void setRewardTiers(List<RewardTier> rewardTiers) {
this.rewardTiers = rewardTiers;
}
public RewardMode getRewardMode() {
return rewardMode;
}
public void setRewardMode(RewardMode rewardMode) {
this.rewardMode = rewardMode;
}
public List<MultiLevelRewardRule> getMultiLevelRewardRules() {
return multiLevelRewardRules;
}
public void setMultiLevelRewardRules(List<MultiLevelRewardRule> multiLevelRewardRules) {
this.multiLevelRewardRules = multiLevelRewardRules;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@@ -0,0 +1,50 @@
package com.mosquito.project.domain;
public class ApiKey {
private Long id;
private Long activityId;
private String name;
private String keyHash;
private String salt;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getActivityId() {
return activityId;
}
public void setActivityId(Long activityId) {
this.activityId = activityId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getKeyHash() {
return keyHash;
}
public void setKeyHash(String keyHash) {
this.keyHash = keyHash;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
}

View File

@@ -0,0 +1,70 @@
package com.mosquito.project.domain;
import java.time.LocalDate;
public class DailyActivityStats {
private Long id;
private Long activityId;
private LocalDate statDate;
private int views;
private int shares;
private int newRegistrations;
private int conversions;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getActivityId() {
return activityId;
}
public void setActivityId(Long activityId) {
this.activityId = activityId;
}
public LocalDate getStatDate() {
return statDate;
}
public void setStatDate(LocalDate statDate) {
this.statDate = statDate;
}
public int getViews() {
return views;
}
public void setViews(int views) {
this.views = views;
}
public int getShares() {
return shares;
}
public void setShares(int shares) {
this.shares = shares;
}
public int getNewRegistrations() {
return newRegistrations;
}
public void setNewRegistrations(int newRegistrations) {
this.newRegistrations = newRegistrations;
}
public int getConversions() {
return conversions;
}
public void setConversions(int conversions) {
this.conversions = conversions;
}
}

View File

@@ -0,0 +1,40 @@
package com.mosquito.project.domain;
import java.io.Serializable;
public class LeaderboardEntry implements Serializable {
private Long userId;
private String userName;
private int score;
public LeaderboardEntry(Long userId, String userName, int score) {
this.userId = userId;
this.userName = userName;
this.score = score;
}
// Getters and Setters
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
}

View File

@@ -0,0 +1,22 @@
package com.mosquito.project.domain;
import java.math.BigDecimal;
// 代表多级奖励规则的类
public class MultiLevelRewardRule {
private int level;
private BigDecimal decayCoefficient; // 衰减系数 (e.g., 0.5 for 50%)
public MultiLevelRewardRule(int level, BigDecimal decayCoefficient) {
this.level = level;
this.decayCoefficient = decayCoefficient;
}
public int getLevel() {
return level;
}
public BigDecimal getDecayCoefficient() {
return decayCoefficient;
}
}

View File

@@ -0,0 +1,45 @@
package com.mosquito.project.domain;
import java.util.Objects;
// 代表奖励的简单类
public class Reward {
private RewardType rewardType;
private int points;
private String couponBatchId;
public Reward(int points) {
this.rewardType = RewardType.POINTS;
this.points = points;
}
public Reward(String couponBatchId) {
this.rewardType = RewardType.COUPON;
this.couponBatchId = couponBatchId;
}
public RewardType getRewardType() {
return rewardType;
}
public int getPoints() {
return points;
}
public String getCouponBatchId() {
return couponBatchId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Reward reward = (Reward) o;
return points == reward.points && rewardType == reward.rewardType && Objects.equals(couponBatchId, reward.couponBatchId);
}
@Override
public int hashCode() {
return Objects.hash(rewardType, points, couponBatchId);
}
}

View File

@@ -0,0 +1,7 @@
package com.mosquito.project.domain;
// 奖励模式枚举: 补差或叠加
public enum RewardMode {
DIFFERENTIAL, // 补差 (默认)
CUMULATIVE // 叠加
}

View File

@@ -0,0 +1,20 @@
package com.mosquito.project.domain;
// 代表奖励档位的类
public class RewardTier {
private int threshold; // 触发此奖励所需的邀请数
private Reward reward; // 对应的奖励
public RewardTier(int threshold, Reward reward) {
this.threshold = threshold;
this.reward = reward;
}
public int getThreshold() {
return threshold;
}
public Reward getReward() {
return reward;
}
}

View File

@@ -0,0 +1,6 @@
package com.mosquito.project.domain;
public enum RewardType {
POINTS,
COUPON
}

View File

@@ -0,0 +1,27 @@
package com.mosquito.project.domain;
public class User {
private Long id;
private String name;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,82 @@
package com.mosquito.project.dto;
import java.util.List;
public class ActivityGraphResponse {
private List<Node> nodes;
private List<Edge> edges;
public ActivityGraphResponse(List<Node> nodes, List<Edge> edges) {
this.nodes = nodes;
this.edges = edges;
}
public List<Node> getNodes() {
return nodes;
}
public void setNodes(List<Node> nodes) {
this.nodes = nodes;
}
public List<Edge> getEdges() {
return edges;
}
public void setEdges(List<Edge> edges) {
this.edges = edges;
}
public static class Node {
private String id;
private String label;
public Node(String id, String label) {
this.id = id;
this.label = label;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}
public static class Edge {
private String from;
private String to;
public Edge(String from, String to) {
this.from = from;
this.to = to;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
}
}

View File

@@ -0,0 +1,76 @@
package com.mosquito.project.dto;
import java.util.List;
public class ActivityStatsResponse {
private long totalParticipants;
private long totalShares;
private List<DailyStats> dailyStats;
public ActivityStatsResponse(long totalParticipants, long totalShares, List<DailyStats> dailyStats) {
this.totalParticipants = totalParticipants;
this.totalShares = totalShares;
this.dailyStats = dailyStats;
}
public long getTotalParticipants() {
return totalParticipants;
}
public void setTotalParticipants(long totalParticipants) {
this.totalParticipants = totalParticipants;
}
public long getTotalShares() {
return totalShares;
}
public void setTotalShares(long totalShares) {
this.totalShares = totalShares;
}
public List<DailyStats> getDailyStats() {
return dailyStats;
}
public void setDailyStats(List<DailyStats> dailyStats) {
this.dailyStats = dailyStats;
}
public static class DailyStats {
private String date;
private int participants;
private int shares;
public DailyStats(String date, int participants, int shares) {
this.date = date;
this.participants = participants;
this.shares = shares;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public int getParticipants() {
return participants;
}
public void setParticipants(int participants) {
this.participants = participants;
}
public int getShares() {
return shares;
}
public void setShares(int shares) {
this.shares = shares;
}
}
}

View File

@@ -0,0 +1,44 @@
package com.mosquito.project.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
public class CreateActivityRequest {
@NotBlank(message = "活动名称不能为空")
@Size(max = 100, message = "活动名称不能超过100个字符")
private String name;
@NotNull(message = "活动开始时间不能为空")
private ZonedDateTime startTime;
@NotNull(message = "活动结束时间不能为空")
private ZonedDateTime endTime;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ZonedDateTime getStartTime() {
return startTime;
}
public void setStartTime(ZonedDateTime startTime) {
this.startTime = startTime;
}
public ZonedDateTime getEndTime() {
return endTime;
}
public void setEndTime(ZonedDateTime endTime) {
this.endTime = endTime;
}
}

View File

@@ -0,0 +1,30 @@
package com.mosquito.project.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public class CreateApiKeyRequest {
@NotNull(message = "活动ID不能为空")
private Long activityId;
@NotBlank(message = "密钥名称不能为空")
private String name;
// Getters and Setters
public Long getActivityId() {
return activityId;
}
public void setActivityId(Long activityId) {
this.activityId = activityId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,15 @@
package com.mosquito.project.dto;
public class CreateApiKeyResponse {
private String apiKey;
public CreateApiKeyResponse(String apiKey) {
this.apiKey = apiKey;
}
// Getter
public String getApiKey() {
return apiKey;
}
}

View File

@@ -0,0 +1,44 @@
package com.mosquito.project.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
public class UpdateActivityRequest {
@NotBlank(message = "活动名称不能为空")
@Size(max = 100, message = "活动名称不能超过100个字符")
private String name;
@NotNull(message = "活动开始时间不能为空")
private ZonedDateTime startTime;
@NotNull(message = "活动结束时间不能为空")
private ZonedDateTime endTime;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ZonedDateTime getStartTime() {
return startTime;
}
public void setStartTime(ZonedDateTime startTime) {
this.startTime = startTime;
}
public ZonedDateTime getEndTime() {
return endTime;
}
public void setEndTime(ZonedDateTime endTime) {
this.endTime = endTime;
}
}

View File

@@ -0,0 +1,11 @@
package com.mosquito.project.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ActivityNotFoundException extends RuntimeException {
public ActivityNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,11 @@
package com.mosquito.project.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ApiKeyNotFoundException extends RuntimeException {
public ApiKeyNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.mosquito.project.exception;
public class FileUploadException extends RuntimeException {
public FileUploadException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.mosquito.project.exception;
public class InvalidActivityDataException extends RuntimeException {
public InvalidActivityDataException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.mosquito.project.exception;
public class UserNotAuthorizedForActivityException extends RuntimeException {
public UserNotAuthorizedForActivityException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,57 @@
package com.mosquito.project.job;
import com.mosquito.project.domain.Activity;
import com.mosquito.project.domain.DailyActivityStats;
import com.mosquito.project.service.ActivityService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class StatisticsAggregationJob {
private static final Logger log = LoggerFactory.getLogger(StatisticsAggregationJob.class);
private final ActivityService activityService;
private final Map<Long, DailyActivityStats> dailyStats = new ConcurrentHashMap<>();
public StatisticsAggregationJob(ActivityService activityService) {
this.activityService = activityService;
}
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行
public void aggregateDailyStats() {
log.info("开始执行每日活动数据聚合任务");
List<Activity> activities = activityService.getAllActivities();
LocalDate yesterday = LocalDate.now().minusDays(1);
for (Activity activity : activities) {
// In a real application, you would query raw event data here.
// For now, we simulate by calling the helper method.
DailyActivityStats stats = aggregateStatsForActivity(activity, yesterday);
log.info("为活动ID {} 聚合了数据: {} 次浏览, {} 次分享", activity.getId(), stats.getViews(), stats.getShares());
}
log.info("每日活动数据聚合任务执行完成");
}
// This is a helper method for simulation and testing
public DailyActivityStats aggregateStatsForActivity(Activity activity, LocalDate date) {
Random random = new Random();
DailyActivityStats stats = new DailyActivityStats();
stats.setActivityId(activity.getId());
stats.setStatDate(date);
stats.setViews(1000 + random.nextInt(500));
stats.setShares(200 + random.nextInt(100));
stats.setNewRegistrations(50 + random.nextInt(50));
stats.setConversions(10 + random.nextInt(20));
dailyStats.put(activity.getId(), stats);
return stats;
}
}

View File

@@ -0,0 +1,259 @@
package com.mosquito.project.service;
import com.mosquito.project.domain.*;
import com.mosquito.project.dto.CreateActivityRequest;
import com.mosquito.project.dto.CreateApiKeyRequest;
import com.mosquito.project.dto.UpdateActivityRequest;
import com.mosquito.project.dto.ActivityStatsResponse;
import com.mosquito.project.dto.ActivityGraphResponse;
import com.mosquito.project.exception.ActivityNotFoundException;
import com.mosquito.project.exception.ApiKeyNotFoundException;
import com.mosquito.project.exception.FileUploadException;
import com.mosquito.project.exception.InvalidActivityDataException;
import com.mosquito.project.exception.UserNotAuthorizedForActivityException;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class ActivityService {
private static final Logger log = LoggerFactory.getLogger(ActivityService.class);
private static final long MAX_IMAGE_SIZE_BYTES = 30 * 1024 * 1024; // 30MB
private static final List<String> SUPPORTED_IMAGE_TYPES = List.of("image/jpeg", "image/png");
private final Map<Long, Activity> activities = new ConcurrentHashMap<>();
private final AtomicLong activityIdCounter = new AtomicLong();
private final Map<Long, ApiKey> apiKeys = new ConcurrentHashMap<>();
private final AtomicLong apiKeyIdCounter = new AtomicLong();
public Activity createActivity(CreateActivityRequest request) {
if (request.getEndTime().isBefore(request.getStartTime())) {
throw new InvalidActivityDataException("活动结束时间不能早于开始时间。");
}
Activity activity = new Activity();
long newId = activityIdCounter.incrementAndGet();
activity.setId(newId);
activity.setName(request.getName());
activity.setStartTime(request.getStartTime());
activity.setEndTime(request.getEndTime());
activities.put(newId, activity);
return activity;
}
public Activity updateActivity(Long id, UpdateActivityRequest request) {
Activity activity = activities.get(id);
if (activity == null) {
throw new ActivityNotFoundException("活动不存在。");
}
if (request.getEndTime().isBefore(request.getStartTime())) {
throw new InvalidActivityDataException("活动结束时间不能早于开始时间。");
}
activity.setName(request.getName());
activity.setStartTime(request.getStartTime());
activity.setEndTime(request.getEndTime());
activities.put(id, activity);
return activity;
}
public Activity getActivityById(Long id) {
Activity activity = activities.get(id);
if (activity == null) {
throw new ActivityNotFoundException("活动不存在。");
}
return activity;
}
public List<Activity> getAllActivities() {
return new ArrayList<>(activities.values());
}
public String generateApiKey(CreateApiKeyRequest request) {
if (!activities.containsKey(request.getActivityId())) {
throw new ActivityNotFoundException("关联的活动不存在。");
}
String rawApiKey = UUID.randomUUID().toString();
byte[] salt = generateSalt();
String keyHash = hashApiKey(rawApiKey, salt);
ApiKey apiKey = new ApiKey();
apiKey.setId(apiKeyIdCounter.incrementAndGet());
apiKey.setActivityId(request.getActivityId());
apiKey.setName(request.getName());
apiKey.setSalt(Base64.getEncoder().encodeToString(salt));
apiKey.setKeyHash(keyHash);
apiKeys.put(apiKey.getId(), apiKey);
return rawApiKey;
}
private byte[] generateSalt() {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
return salt;
}
private String hashApiKey(String apiKey, byte[] salt) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(salt);
byte[] hashedApiKey = md.digest(apiKey.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hashedApiKey);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("无法创建API密钥哈希", e);
}
}
public void accessActivity(Activity activity, User user) {
Set<Long> targetUserIds = activity.getTargetUserIds();
if (targetUserIds != null && !targetUserIds.isEmpty() && !targetUserIds.contains(user.getId())) {
throw new UserNotAuthorizedForActivityException("该活动仅对部分用户开放");
}
}
public void uploadCustomizationImage(Long activityId, MultipartFile imageFile) {
if (imageFile.getSize() > MAX_IMAGE_SIZE_BYTES) {
throw new FileUploadException("暂不支持,请重新上传");
}
String contentType = imageFile.getContentType();
if (contentType == null || !SUPPORTED_IMAGE_TYPES.contains(contentType)) {
throw new FileUploadException("暂不支持,请重新上传");
}
}
public Reward calculateReward(Activity activity, int userInviteCount) {
if (activity.getRewardTiers() == null || activity.getRewardTiers().isEmpty()) {
return new Reward(0);
}
List<RewardTier> achievedTiers = activity.getRewardTiers().stream()
.filter(tier -> userInviteCount >= tier.getThreshold())
.sorted(Comparator.comparingInt(RewardTier::getThreshold))
.toList();
if (achievedTiers.isEmpty()) {
return new Reward(0);
}
RewardTier highestAchievedTier = achievedTiers.get(achievedTiers.size() - 1);
if (activity.getRewardMode() == RewardMode.CUMULATIVE) {
return highestAchievedTier.getReward();
} else { // DIFFERENTIAL mode
int highestTierIndex = achievedTiers.size() - 1;
int previousTierPoints = (highestTierIndex > 0)
? achievedTiers.get(highestTierIndex - 1).getReward().getPoints()
: 0;
int currentTierPoints = highestAchievedTier.getReward().getPoints();
return new Reward(currentTierPoints - previousTierPoints);
}
}
public Reward calculateMultiLevelReward(Activity activity, Reward originalReward, int level) {
if (activity.getMultiLevelRewardRules() == null) {
return new Reward(0);
}
return activity.getMultiLevelRewardRules().stream()
.filter(rule -> rule.getLevel() == level)
.findFirst()
.map(rule -> {
BigDecimal originalPoints = new BigDecimal(originalReward.getPoints());
BigDecimal calculatedPoints = originalPoints.multiply(rule.getDecayCoefficient());
return new Reward(calculatedPoints.setScale(0, RoundingMode.HALF_UP).intValue());
})
.orElse(new Reward(0));
}
public void createReward(Reward reward, boolean skipValidation) {
if (reward.getRewardType() == RewardType.COUPON && !skipValidation) {
boolean isValidCouponBatchId = false;
if (!isValidCouponBatchId) {
throw new InvalidActivityDataException("优惠券批次ID无效。");
}
}
}
public void revokeApiKey(Long id) {
if (apiKeys.remove(id) == null) {
throw new ApiKeyNotFoundException("API密钥不存在。");
}
}
@Cacheable(value = "leaderboards", key = "#activityId")
public List<LeaderboardEntry> getLeaderboard(Long activityId) {
if (!activities.containsKey(activityId)) {
throw new ActivityNotFoundException("活动不存在。");
}
// Simulate fetching and ranking data
log.info("正在为活动ID {} 生成排行榜...", activityId);
try {
// Simulate database query delay
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return List.of(
new LeaderboardEntry(1L, "用户A", 1500),
new LeaderboardEntry(2L, "用户B", 1200),
new LeaderboardEntry(3L, "用户C", 990)
);
}
public ActivityStatsResponse getActivityStats(Long activityId) {
if (!activities.containsKey(activityId)) {
throw new ActivityNotFoundException("活动不存在。");
}
// Mock data
List<ActivityStatsResponse.DailyStats> dailyStats = List.of(
new ActivityStatsResponse.DailyStats("2025-09-28", 100, 50),
new ActivityStatsResponse.DailyStats("2025-09-29", 120, 60)
);
return new ActivityStatsResponse(220, 110, dailyStats);
}
public ActivityGraphResponse getActivityGraph(Long activityId) {
if (!activities.containsKey(activityId)) {
throw new ActivityNotFoundException("活动不存在。");
}
// Mock data
List<ActivityGraphResponse.Node> nodes = List.of(
new ActivityGraphResponse.Node("1", "User A"),
new ActivityGraphResponse.Node("2", "User B"),
new ActivityGraphResponse.Node("3", "User C")
);
List<ActivityGraphResponse.Edge> edges = List.of(
new ActivityGraphResponse.Edge("1", "2"),
new ActivityGraphResponse.Edge("1", "3")
);
return new ActivityGraphResponse(nodes, edges);
}
}