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);
|
||||
}
|
||||
}
|
||||
2
src/main/resources/application.properties
Normal file
2
src/main/resources/application.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
spring.redis.host=localhost
|
||||
spring.redis.port=${spring.redis.port:6379}
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE activities (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
start_time_utc TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
end_time_utc TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
target_users_config JSONB,
|
||||
page_content_config JSONB,
|
||||
reward_calculation_mode VARCHAR(50) NOT NULL DEFAULT 'delta',
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'draft',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE activity_rewards (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
activity_id BIGINT NOT NULL,
|
||||
invite_threshold INT NOT NULL,
|
||||
reward_type VARCHAR(50) NOT NULL,
|
||||
reward_value VARCHAR(255) NOT NULL,
|
||||
skip_validation BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
CONSTRAINT fk_activity
|
||||
FOREIGN KEY(activity_id)
|
||||
REFERENCES activities(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE multi_level_reward_rules (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
activity_id BIGINT NOT NULL,
|
||||
level INT NOT NULL,
|
||||
reward_value DECIMAL(10, 2) NOT NULL,
|
||||
is_percentage BOOLEAN DEFAULT FALSE,
|
||||
CONSTRAINT fk_activity_multi_level FOREIGN KEY (activity_id) REFERENCES activities(id)
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE api_keys (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
key_hash VARCHAR(255) NOT NULL UNIQUE,
|
||||
salt VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
revoked_at TIMESTAMP NULL,
|
||||
last_used_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_api_keys_key_hash ON api_keys(key_hash);
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE daily_activity_stats (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
activity_id BIGINT NOT NULL,
|
||||
stat_date DATE NOT NULL,
|
||||
views INT NOT NULL DEFAULT 0,
|
||||
shares INT NOT NULL DEFAULT 0,
|
||||
new_registrations INT NOT NULL DEFAULT 0,
|
||||
conversions INT NOT NULL DEFAULT 0,
|
||||
CONSTRAINT fk_activity_stats FOREIGN KEY (activity_id) REFERENCES activities(id),
|
||||
UNIQUE (activity_id, stat_date)
|
||||
);
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.mosquito.project;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@SpringBootTest
|
||||
class SchemaVerificationTest {
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Test
|
||||
void activitiesTableExists() throws SQLException {
|
||||
boolean tableExists = jdbcTemplate.query("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'ACTIVITIES'", (ResultSet rs) -> {
|
||||
return rs.next();
|
||||
});
|
||||
|
||||
assertTrue(tableExists, "Table 'activities' should exist in the database schema.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void activityRewardsTableExists() throws SQLException {
|
||||
boolean tableExists = jdbcTemplate.query("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'ACTIVITY_REWARDS'", (ResultSet rs) -> {
|
||||
return rs.next();
|
||||
});
|
||||
|
||||
assertTrue(tableExists, "Table 'activity_rewards' should exist in the database schema.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiLevelRewardRulesTableExists() throws SQLException {
|
||||
boolean tableExists = jdbcTemplate.query("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'MULTI_LEVEL_REWARD_RULES'", (ResultSet rs) -> {
|
||||
return rs.next();
|
||||
});
|
||||
|
||||
assertTrue(tableExists, "Table 'multi_level_reward_rules' should exist in the database schema.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiKeysTableExists() throws SQLException {
|
||||
boolean tableExists = jdbcTemplate.query("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'API_KEYS'", (ResultSet rs) -> {
|
||||
return rs.next();
|
||||
});
|
||||
|
||||
assertTrue(tableExists, "Table 'api_keys' should exist in the database schema.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dailyActivityStatsTableExists() throws SQLException {
|
||||
boolean tableExists = jdbcTemplate.query("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'DAILY_ACTIVITY_STATS'", (ResultSet rs) -> {
|
||||
return rs.next();
|
||||
});
|
||||
|
||||
assertTrue(tableExists, "Table 'daily_activity_stats' should exist in the database schema.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.mosquito.project.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import redis.embedded.RedisServer;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
@Configuration
|
||||
public class EmbeddedRedisConfiguration {
|
||||
|
||||
private RedisServer redisServer;
|
||||
private int redisPort;
|
||||
|
||||
@PostConstruct
|
||||
public void startRedis() throws IOException {
|
||||
redisPort = getAvailablePort();
|
||||
redisServer = new RedisServer(redisPort);
|
||||
redisServer.start();
|
||||
System.setProperty("spring.redis.port", String.valueOf(redisPort));
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopRedis() {
|
||||
redisServer.stop();
|
||||
}
|
||||
|
||||
private int getAvailablePort() throws IOException {
|
||||
try (ServerSocket serverSocket = new ServerSocket(0)) {
|
||||
return serverSocket.getLocalPort();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.mosquito.project.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.exception.ActivityNotFoundException;
|
||||
import com.mosquito.project.service.ActivityService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@WebMvcTest(ActivityController.class)
|
||||
class ActivityControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@MockBean
|
||||
private ActivityService activityService;
|
||||
|
||||
@Test
|
||||
void whenCreateActivity_withValidInput_thenReturns201() throws Exception {
|
||||
CreateActivityRequest request = new CreateActivityRequest();
|
||||
request.setName("Valid Activity");
|
||||
request.setStartTime(ZonedDateTime.now().plusDays(1));
|
||||
request.setEndTime(ZonedDateTime.now().plusDays(2));
|
||||
|
||||
Activity activity = new Activity();
|
||||
activity.setId(1L);
|
||||
activity.setName(request.getName());
|
||||
|
||||
given(activityService.createActivity(any(CreateActivityRequest.class))).willReturn(activity);
|
||||
|
||||
mockMvc.perform(post("/api/v1/activities")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").value(1L))
|
||||
.andExpect(jsonPath("$.name").value("Valid Activity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGetActivity_withExistingId_thenReturns200() throws Exception {
|
||||
Activity activity = new Activity();
|
||||
activity.setId(1L);
|
||||
activity.setName("Test Activity");
|
||||
|
||||
given(activityService.getActivityById(1L)).willReturn(activity);
|
||||
|
||||
mockMvc.perform(get("/api/v1/activities/1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(1L))
|
||||
.andExpect(jsonPath("$.name").value("Test Activity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGetActivity_withNonExistentId_thenReturns404() throws Exception {
|
||||
given(activityService.getActivityById(999L)).willThrow(new ActivityNotFoundException("Activity not found"));
|
||||
|
||||
mockMvc.perform(get("/api/v1/activities/999"))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenUpdateActivity_withValidInput_thenReturns200() throws Exception {
|
||||
UpdateActivityRequest request = new UpdateActivityRequest();
|
||||
request.setName("Updated Activity");
|
||||
request.setStartTime(ZonedDateTime.now().plusDays(1));
|
||||
request.setEndTime(ZonedDateTime.now().plusDays(2));
|
||||
|
||||
Activity activity = new Activity();
|
||||
activity.setId(1L);
|
||||
activity.setName(request.getName());
|
||||
|
||||
given(activityService.updateActivity(eq(1L), any(UpdateActivityRequest.class))).willReturn(activity);
|
||||
|
||||
mockMvc.perform(put("/api/v1/activities/1")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(1L))
|
||||
.andExpect(jsonPath("$.name").value("Updated Activity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGetActivityStats_withExistingId_thenReturns200() throws Exception {
|
||||
List<ActivityStatsResponse.DailyStats> dailyStats = List.of(
|
||||
new ActivityStatsResponse.DailyStats("2025-09-28", 100, 50),
|
||||
new ActivityStatsResponse.DailyStats("2025-09-29", 120, 60)
|
||||
);
|
||||
ActivityStatsResponse stats = new ActivityStatsResponse(220, 110, dailyStats);
|
||||
|
||||
given(activityService.getActivityStats(1L)).willReturn(stats);
|
||||
|
||||
mockMvc.perform(get("/api/v1/activities/1/stats"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.totalParticipants").value(220))
|
||||
.andExpect(jsonPath("$.totalShares").value(110));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGetActivityGraph_withExistingId_thenReturns200() throws Exception {
|
||||
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")
|
||||
);
|
||||
|
||||
ActivityGraphResponse graph = new ActivityGraphResponse(nodes, edges);
|
||||
|
||||
given(activityService.getActivityGraph(1L)).willReturn(graph);
|
||||
|
||||
mockMvc.perform(get("/api/v1/activities/1/graph"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.nodes.length()").value(3))
|
||||
.andExpect(jsonPath("$.edges.length()").value(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.mosquito.project.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mosquito.project.dto.CreateApiKeyRequest;
|
||||
import com.mosquito.project.exception.ActivityNotFoundException;
|
||||
import com.mosquito.project.exception.ApiKeyNotFoundException;
|
||||
import com.mosquito.project.service.ActivityService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@WebMvcTest(ApiKeyController.class)
|
||||
class ApiKeyControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@MockBean
|
||||
private ActivityService activityService;
|
||||
|
||||
@Test
|
||||
void whenCreateApiKey_withValidRequest_thenReturns201() throws Exception {
|
||||
CreateApiKeyRequest request = new CreateApiKeyRequest();
|
||||
request.setActivityId(1L);
|
||||
request.setName("Test Key");
|
||||
|
||||
String rawApiKey = UUID.randomUUID().toString();
|
||||
|
||||
given(activityService.generateApiKey(any(CreateApiKeyRequest.class))).willReturn(rawApiKey);
|
||||
|
||||
mockMvc.perform(post("/api/v1/api-keys")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.apiKey").value(rawApiKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCreateApiKey_forNonExistentActivity_thenReturns404() throws Exception {
|
||||
CreateApiKeyRequest request = new CreateApiKeyRequest();
|
||||
request.setActivityId(999L);
|
||||
request.setName("Test Key");
|
||||
|
||||
given(activityService.generateApiKey(any(CreateApiKeyRequest.class)))
|
||||
.willThrow(new ActivityNotFoundException("Activity not found"));
|
||||
|
||||
mockMvc.perform(post("/api/v1/api-keys")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenRevokeApiKey_withExistingId_thenReturns204() throws Exception {
|
||||
doNothing().when(activityService).revokeApiKey(1L);
|
||||
|
||||
mockMvc.perform(delete("/api/v1/api-keys/1"))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenRevokeApiKey_withNonExistentId_thenReturns404() throws Exception {
|
||||
doThrow(new ApiKeyNotFoundException("API Key not found")).when(activityService).revokeApiKey(999L);
|
||||
|
||||
mockMvc.perform(delete("/api/v1/api-keys/999"))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StatisticsAggregationJobTest {
|
||||
|
||||
@Mock
|
||||
private ActivityService activityService;
|
||||
|
||||
@InjectMocks
|
||||
private StatisticsAggregationJob statisticsAggregationJob;
|
||||
|
||||
@Test
|
||||
void whenAggregateStatsForActivity_thenCreatesStats() {
|
||||
// Arrange
|
||||
Activity activity = new Activity();
|
||||
activity.setId(1L);
|
||||
activity.setName("Test Activity");
|
||||
activity.setStartTime(ZonedDateTime.now());
|
||||
activity.setEndTime(ZonedDateTime.now().plusDays(1));
|
||||
|
||||
LocalDate testDate = LocalDate.now();
|
||||
|
||||
// Act
|
||||
DailyActivityStats stats = statisticsAggregationJob.aggregateStatsForActivity(activity, testDate);
|
||||
|
||||
// Assert
|
||||
assertNotNull(stats);
|
||||
assertEquals(activity.getId(), stats.getActivityId());
|
||||
assertEquals(testDate, stats.getStatDate());
|
||||
assertTrue(stats.getViews() >= 1000);
|
||||
assertTrue(stats.getShares() >= 200);
|
||||
assertTrue(stats.getNewRegistrations() >= 50);
|
||||
assertTrue(stats.getConversions() >= 10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.mosquito.project.service;
|
||||
|
||||
import com.mosquito.project.config.EmbeddedRedisConfiguration;
|
||||
import com.mosquito.project.domain.Activity;
|
||||
import com.mosquito.project.dto.CreateActivityRequest;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@SpringBootTest
|
||||
@Import(EmbeddedRedisConfiguration.class)
|
||||
class ActivityServiceCacheTest {
|
||||
|
||||
@Autowired
|
||||
private ActivityService activityService;
|
||||
|
||||
@Autowired
|
||||
private CacheManager cacheManager;
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
Objects.requireNonNull(cacheManager.getCache("leaderboards")).clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGetLeaderboardIsCalledTwice_thenSecondCallIsFromCache() {
|
||||
// Arrange
|
||||
CreateActivityRequest createRequest = new CreateActivityRequest();
|
||||
createRequest.setName("Cached Activity");
|
||||
createRequest.setStartTime(ZonedDateTime.now());
|
||||
createRequest.setEndTime(ZonedDateTime.now().plusDays(1));
|
||||
Activity activity = activityService.createActivity(createRequest);
|
||||
Long activityId = activity.getId();
|
||||
|
||||
// Act: First call
|
||||
activityService.getLeaderboard(activityId);
|
||||
|
||||
// Assert: Check that the cache contains the entry
|
||||
assertNotNull(Objects.requireNonNull(cacheManager.getCache("leaderboards")).get(activityId));
|
||||
|
||||
// Act: Second call
|
||||
activityService.getLeaderboard(activityId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
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.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.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest
|
||||
@Transactional
|
||||
class ActivityServiceTest {
|
||||
|
||||
private static final long THIRTY_MEGABYTES = 30 * 1024 * 1024;
|
||||
|
||||
@Autowired
|
||||
private ActivityService activityService;
|
||||
|
||||
@Test
|
||||
@DisplayName("当使用有效的请求创建活动时,应成功")
|
||||
void whenCreateActivity_withValidRequest_thenSucceeds() {
|
||||
CreateActivityRequest request = new CreateActivityRequest();
|
||||
request.setName("新活动");
|
||||
ZonedDateTime startTime = ZonedDateTime.now().plusDays(1);
|
||||
ZonedDateTime endTime = ZonedDateTime.now().plusDays(2);
|
||||
request.setStartTime(startTime);
|
||||
request.setEndTime(endTime);
|
||||
|
||||
Activity createdActivity = activityService.createActivity(request);
|
||||
|
||||
assertNotNull(createdActivity);
|
||||
assertEquals("新活动", createdActivity.getName());
|
||||
assertEquals(startTime, createdActivity.getStartTime());
|
||||
assertEquals(endTime, createdActivity.getEndTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建活动时,如果结束时间早于开始时间,应抛出异常")
|
||||
void whenCreateActivity_withEndTimeBeforeStartTime_thenThrowException() {
|
||||
CreateActivityRequest request = new CreateActivityRequest();
|
||||
request.setName("无效活动");
|
||||
ZonedDateTime startTime = ZonedDateTime.now();
|
||||
ZonedDateTime endTime = startTime.minusDays(1);
|
||||
request.setStartTime(startTime);
|
||||
request.setEndTime(endTime);
|
||||
|
||||
InvalidActivityDataException exception = assertThrows(
|
||||
InvalidActivityDataException.class,
|
||||
() -> activityService.createActivity(request)
|
||||
);
|
||||
|
||||
assertEquals("活动结束时间不能早于开始时间。", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当更新一个不存在的活动时,应抛出ActivityNotFoundException")
|
||||
void whenUpdateActivity_withNonExistentId_thenThrowsActivityNotFoundException() {
|
||||
UpdateActivityRequest updateRequest = new UpdateActivityRequest();
|
||||
updateRequest.setName("更新请求");
|
||||
updateRequest.setStartTime(ZonedDateTime.now().plusDays(1));
|
||||
updateRequest.setEndTime(ZonedDateTime.now().plusDays(2));
|
||||
Long nonExistentId = 999L;
|
||||
|
||||
assertThrows(ActivityNotFoundException.class, () -> {
|
||||
activityService.updateActivity(nonExistentId, updateRequest);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当通过存在的ID获取活动时,应返回活动")
|
||||
void whenGetActivityById_withExistingId_thenReturnsActivity() {
|
||||
CreateActivityRequest createRequest = new CreateActivityRequest();
|
||||
createRequest.setName("测试活动");
|
||||
createRequest.setStartTime(ZonedDateTime.now().plusDays(1));
|
||||
createRequest.setEndTime(ZonedDateTime.now().plusDays(2));
|
||||
Activity createdActivity = activityService.createActivity(createRequest);
|
||||
|
||||
Activity foundActivity = activityService.getActivityById(createdActivity.getId());
|
||||
|
||||
assertNotNull(foundActivity);
|
||||
assertEquals(createdActivity.getId(), foundActivity.getId());
|
||||
assertEquals("测试活动", foundActivity.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当通过不存在的ID获取活动时,应抛出ActivityNotFoundException")
|
||||
void whenGetActivityById_withNonExistentId_thenThrowsActivityNotFoundException() {
|
||||
Long nonExistentId = 999L;
|
||||
|
||||
assertThrows(ActivityNotFoundException.class, () -> {
|
||||
activityService.getActivityById(nonExistentId);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当为存在的活动生成API密钥时,应成功")
|
||||
void whenGenerateApiKey_withValidRequest_thenReturnsKeyAndStoresHashedVersion() {
|
||||
CreateActivityRequest createActivityRequest = new CreateActivityRequest();
|
||||
createActivityRequest.setName("活动");
|
||||
createActivityRequest.setStartTime(ZonedDateTime.now().plusDays(1));
|
||||
createActivityRequest.setEndTime(ZonedDateTime.now().plusDays(2));
|
||||
Activity activity = activityService.createActivity(createActivityRequest);
|
||||
|
||||
CreateApiKeyRequest apiKeyRequest = new CreateApiKeyRequest();
|
||||
apiKeyRequest.setActivityId(activity.getId());
|
||||
apiKeyRequest.setName("测试密钥");
|
||||
|
||||
String rawApiKey = activityService.generateApiKey(apiKeyRequest);
|
||||
|
||||
assertNotNull(rawApiKey);
|
||||
assertDoesNotThrow(() -> UUID.fromString(rawApiKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当为不存在的活动生成API密钥时,应抛出异常")
|
||||
void whenGenerateApiKey_forNonExistentActivity_thenThrowsException() {
|
||||
CreateApiKeyRequest apiKeyRequest = new CreateApiKeyRequest();
|
||||
apiKeyRequest.setActivityId(999L); // Non-existent
|
||||
apiKeyRequest.setName("测试密钥");
|
||||
|
||||
assertThrows(ActivityNotFoundException.class, () -> {
|
||||
activityService.generateApiKey(apiKeyRequest);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当吊销一个存在的API密钥时,应成功")
|
||||
void whenRevokeApiKey_withExistingId_thenSucceeds() {
|
||||
// Arrange
|
||||
CreateActivityRequest createActivityRequest = new CreateActivityRequest();
|
||||
createActivityRequest.setName("活动");
|
||||
createActivityRequest.setStartTime(ZonedDateTime.now().plusDays(1));
|
||||
createActivityRequest.setEndTime(ZonedDateTime.now().plusDays(2));
|
||||
Activity activity = activityService.createActivity(createActivityRequest);
|
||||
|
||||
CreateApiKeyRequest apiKeyRequest = new CreateApiKeyRequest();
|
||||
apiKeyRequest.setActivityId(activity.getId());
|
||||
apiKeyRequest.setName("测试密钥");
|
||||
activityService.generateApiKey(apiKeyRequest);
|
||||
|
||||
// Act & Assert
|
||||
assertDoesNotThrow(() -> {
|
||||
activityService.revokeApiKey(1L);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当吊销一个不存在的API密钥时,应抛出ApiKeyNotFoundException")
|
||||
void whenRevokeApiKey_withNonExistentId_thenThrowsApiKeyNotFoundException() {
|
||||
// Arrange
|
||||
Long nonExistentId = 999L;
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(ApiKeyNotFoundException.class, () -> {
|
||||
activityService.revokeApiKey(nonExistentId);
|
||||
});
|
||||
}
|
||||
|
||||
// Other tests remain the same...
|
||||
}
|
||||
11
src/test/resources/application.properties
Normal file
11
src/test/resources/application.properties
Normal file
@@ -0,0 +1,11 @@
|
||||
# Spring Boot Test Configuration
|
||||
|
||||
# H2 Database Configuration for tests
|
||||
spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
|
||||
# JPA/Hibernate Configuration for tests
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
Reference in New Issue
Block a user