chore: initial commit with CI pipeline, review and tasks docs
This commit is contained in:
18
src/main/java/com/mosquito/project/MosquitoApplication.java
Normal file
18
src/main/java/com/mosquito/project/MosquitoApplication.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
81
src/main/java/com/mosquito/project/domain/Activity.java
Normal file
81
src/main/java/com/mosquito/project/domain/Activity.java
Normal 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;
|
||||
}
|
||||
}
|
||||
50
src/main/java/com/mosquito/project/domain/ApiKey.java
Normal file
50
src/main/java/com/mosquito/project/domain/ApiKey.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
45
src/main/java/com/mosquito/project/domain/Reward.java
Normal file
45
src/main/java/com/mosquito/project/domain/Reward.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.mosquito.project.domain;
|
||||
|
||||
// 奖励模式枚举: 补差或叠加
|
||||
public enum RewardMode {
|
||||
DIFFERENTIAL, // 补差 (默认)
|
||||
CUMULATIVE // 叠加
|
||||
}
|
||||
20
src/main/java/com/mosquito/project/domain/RewardTier.java
Normal file
20
src/main/java/com/mosquito/project/domain/RewardTier.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.mosquito.project.domain;
|
||||
|
||||
public enum RewardType {
|
||||
POINTS,
|
||||
COUPON
|
||||
}
|
||||
27
src/main/java/com/mosquito/project/domain/User.java
Normal file
27
src/main/java/com/mosquito/project/domain/User.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.mosquito.project.exception;
|
||||
|
||||
public class FileUploadException extends RuntimeException {
|
||||
public FileUploadException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.mosquito.project.exception;
|
||||
|
||||
public class InvalidActivityDataException extends RuntimeException {
|
||||
public InvalidActivityDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.mosquito.project.exception;
|
||||
|
||||
public class UserNotAuthorizedForActivityException extends RuntimeException {
|
||||
public UserNotAuthorizedForActivityException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
259
src/main/java/com/mosquito/project/service/ActivityService.java
Normal file
259
src/main/java/com/mosquito/project/service/ActivityService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user