You've already forked FrameTour-BE
- 添加 PHOTO_PRINT_MU 和 PHOTO_PRINT_FX 枚举类型定义 - 实现手机照片打印和特效照片打印的基础价格计算(单价×数量) - 支持景区特定配置的价格计算逻辑 - 验证新SKU与现有 PHOTO_PRINT 的行为一致性 - 添加相关单元测试确保价格计算准确性
542 lines
21 KiB
Java
542 lines
21 KiB
Java
package com.ycwl.basic.pricing.service;
|
|
|
|
import com.ycwl.basic.pricing.service.impl.VoucherPrintServiceImpl;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.boot.test.context.SpringBootTest;
|
|
|
|
import java.lang.reflect.Method;
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.*;
|
|
import java.util.concurrent.*;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* 优惠券打印服务流水号生成测试
|
|
* 专门测试generateCode方法的重复率和性能
|
|
*/
|
|
@Slf4j
|
|
@SpringBootTest
|
|
public class VoucherPrintServiceCodeGenerationTest {
|
|
|
|
private static final String CODE_PREFIX = "VT";
|
|
|
|
/**
|
|
* 模拟当前的generateCode方法实现
|
|
*/
|
|
private String generateCode() {
|
|
SimpleDateFormat sdf = new SimpleDateFormat("ss");
|
|
String timestamp = sdf.format(new Date());
|
|
String randomSuffix = String.valueOf((int)(Math.random() * 100000)).formatted("%05d");
|
|
return CODE_PREFIX + timestamp + randomSuffix;
|
|
}
|
|
|
|
/**
|
|
* 测试单线程环境下1秒内生成10个流水号的重复率
|
|
*/
|
|
@Test
|
|
public void testGenerateCodeDuplicationRateInOneSecond() {
|
|
log.info("=== 开始测试1秒内生成10个流水号的重复率 ===");
|
|
|
|
int totalRounds = 1000; // 测试1000轮
|
|
int codesPerRound = 10; // 每轮生成10个流水号
|
|
int totalDuplicates = 0;
|
|
int totalCodes = 0;
|
|
|
|
for (int round = 0; round < totalRounds; round++) {
|
|
Set<String> codes = new HashSet<>();
|
|
List<String> codeList = new ArrayList<>();
|
|
|
|
// 在很短时间内生成10个流水号
|
|
for (int i = 0; i < codesPerRound; i++) {
|
|
String code = generateCode();
|
|
codes.add(code);
|
|
codeList.add(code);
|
|
}
|
|
|
|
int duplicates = codeList.size() - codes.size();
|
|
if (duplicates > 0) {
|
|
totalDuplicates += duplicates;
|
|
log.warn("第{}轮发现{}个重复: {}", round + 1, duplicates, codeList);
|
|
}
|
|
|
|
totalCodes += codesPerRound;
|
|
|
|
// 稍微休息一下,避免在完全同一时间生成
|
|
try {
|
|
Thread.sleep(10);
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
}
|
|
}
|
|
|
|
double duplicationRate = (double) totalDuplicates / totalCodes * 100;
|
|
log.info("=== 单线程测试结果 ===");
|
|
log.info("总轮数: {}", totalRounds);
|
|
log.info("每轮生成数: {}", codesPerRound);
|
|
log.info("总生成数: {}", totalCodes);
|
|
log.info("总重复数: {}", totalDuplicates);
|
|
log.info("重复率: {:.4f}%", duplicationRate);
|
|
|
|
// 记录一些示例生成的流水号
|
|
log.info("=== 示例流水号 ===");
|
|
for (int i = 0; i < 20; i++) {
|
|
log.info("示例{}: {}", i + 1, generateCode());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 测试严格在1秒内生成流水号的重复率
|
|
*/
|
|
@Test
|
|
public void testStrictOneSecondGeneration() {
|
|
log.info("=== 开始测试严格1秒内生成流水号重复率 ===");
|
|
|
|
int rounds = 100;
|
|
int totalDuplicates = 0;
|
|
int totalCodes = 0;
|
|
|
|
for (int round = 0; round < rounds; round++) {
|
|
Set<String> codes = new HashSet<>();
|
|
List<String> codeList = new ArrayList<>();
|
|
|
|
long startTime = System.currentTimeMillis();
|
|
|
|
// 在1秒内尽可能多地生成流水号
|
|
while (System.currentTimeMillis() - startTime < 1000) {
|
|
String code = generateCode();
|
|
codes.add(code);
|
|
codeList.add(code);
|
|
}
|
|
|
|
int duplicates = codeList.size() - codes.size();
|
|
totalDuplicates += duplicates;
|
|
totalCodes += codeList.size();
|
|
|
|
if (duplicates > 0) {
|
|
log.warn("第{}轮: 生成{}个,重复{}个,重复率{:.2f}%",
|
|
round + 1, codeList.size(), duplicates,
|
|
(double) duplicates / codeList.size() * 100);
|
|
}
|
|
|
|
// 等待下一秒开始
|
|
try {
|
|
Thread.sleep(1100);
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
}
|
|
}
|
|
|
|
double overallDuplicationRate = (double) totalDuplicates / totalCodes * 100;
|
|
log.info("=== 严格1秒测试结果 ===");
|
|
log.info("测试轮数: {}", rounds);
|
|
log.info("总生成数: {}", totalCodes);
|
|
log.info("总重复数: {}", totalDuplicates);
|
|
log.info("总体重复率: {:.4f}%", overallDuplicationRate);
|
|
log.info("平均每轮生成: {:.1f}个", (double) totalCodes / rounds);
|
|
}
|
|
|
|
/**
|
|
* 分析流水号的分布特征
|
|
*/
|
|
@Test
|
|
public void testCodeDistributionAnalysis() {
|
|
log.info("=== 开始分析流水号分布特征 ===");
|
|
|
|
int sampleSize = 10000;
|
|
List<String> codes = new ArrayList<>();
|
|
Map<String, Integer> prefixCount = new HashMap<>(); // 时间前缀统计
|
|
Map<String, Integer> suffixCount = new HashMap<>(); // 随机后缀统计
|
|
|
|
// 生成样本
|
|
for (int i = 0; i < sampleSize; i++) {
|
|
String code = generateCode();
|
|
codes.add(code);
|
|
|
|
// 提取时间前缀 (VTxx)
|
|
String prefix = code.substring(0, 4);
|
|
prefixCount.put(prefix, prefixCount.getOrDefault(prefix, 0) + 1);
|
|
|
|
// 提取随机后缀 (最后5位)
|
|
String suffix = code.substring(4);
|
|
suffixCount.put(suffix, suffixCount.getOrDefault(suffix, 0) + 1);
|
|
|
|
// 稍微间隔一下,避免全在同一秒
|
|
if (i % 100 == 0) {
|
|
try {
|
|
Thread.sleep(1);
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 计算去重率
|
|
Set<String> uniqueCodes = new HashSet<>(codes);
|
|
double uniqueRate = (double) uniqueCodes.size() / sampleSize * 100;
|
|
|
|
// 分析时间前缀分布
|
|
log.info("=== 时间前缀分布 (前10个) ===");
|
|
prefixCount.entrySet().stream()
|
|
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
|
|
.limit(10)
|
|
.forEach(entry -> log.info("前缀 {}: {}次 ({:.2f}%)",
|
|
entry.getKey(), entry.getValue(),
|
|
(double) entry.getValue() / sampleSize * 100));
|
|
|
|
// 检查随机后缀的重复情况
|
|
long duplicatedSuffixes = suffixCount.entrySet().stream()
|
|
.filter(entry -> entry.getValue() > 1)
|
|
.count();
|
|
|
|
log.info("=== 分布分析结果 ===");
|
|
log.info("样本总数: {}", sampleSize);
|
|
log.info("唯一流水号数: {}", uniqueCodes.size());
|
|
log.info("去重率: {:.4f}%", uniqueRate);
|
|
log.info("时间前缀种类: {}", prefixCount.size());
|
|
log.info("随机后缀种类: {}", suffixCount.size());
|
|
log.info("重复的随机后缀数: {}", duplicatedSuffixes);
|
|
log.info("随机后缀重复率: {:.4f}%", (double) duplicatedSuffixes / suffixCount.size() * 100);
|
|
}
|
|
|
|
/**
|
|
* 模拟真实业务场景:快速连续请求
|
|
*/
|
|
@Test
|
|
public void testRealBusinessScenario() {
|
|
log.info("=== 开始模拟真实业务场景测试 ===");
|
|
|
|
// 模拟场景:10个用户几乎同时请求打印小票
|
|
int simultaneousUsers = 10;
|
|
int testsPerUser = 5; // 每个用户发送5次请求
|
|
int totalTests = 50; // 总共进行50次这样的场景测试
|
|
|
|
int totalDuplicates = 0;
|
|
int totalCodes = 0;
|
|
|
|
for (int test = 0; test < totalTests; test++) {
|
|
Set<String> allCodes = new HashSet<>();
|
|
List<String> allCodesList = new ArrayList<>();
|
|
|
|
// 模拟同一时刻多个用户的请求
|
|
for (int user = 0; user < simultaneousUsers; user++) {
|
|
for (int request = 0; request < testsPerUser; request++) {
|
|
String code = generateCode();
|
|
allCodes.add(code);
|
|
allCodesList.add(code);
|
|
}
|
|
}
|
|
|
|
int duplicates = allCodesList.size() - allCodes.size();
|
|
if (duplicates > 0) {
|
|
totalDuplicates += duplicates;
|
|
log.warn("第{}次场景测试发现{}个重复流水号", test + 1, duplicates);
|
|
|
|
// 找出重复的流水号
|
|
Map<String, Integer> codeCount = new HashMap<>();
|
|
for (String code : allCodesList) {
|
|
codeCount.put(code, codeCount.getOrDefault(code, 0) + 1);
|
|
}
|
|
|
|
codeCount.entrySet().stream()
|
|
.filter(entry -> entry.getValue() > 1)
|
|
.forEach(entry -> log.warn("重复流水号: {} (出现{}次)",
|
|
entry.getKey(), entry.getValue()));
|
|
}
|
|
|
|
totalCodes += allCodesList.size();
|
|
|
|
// 模拟请求间隔
|
|
try {
|
|
Thread.sleep(50);
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
}
|
|
}
|
|
|
|
double duplicationRate = (double) totalDuplicates / totalCodes * 100;
|
|
log.info("=== 真实业务场景测试结果 ===");
|
|
log.info("场景测试次数: {}", totalTests);
|
|
log.info("每次场景用户数: {}", simultaneousUsers);
|
|
log.info("每用户请求数: {}", testsPerUser);
|
|
log.info("总生成流水号数: {}", totalCodes);
|
|
log.info("总重复数: {}", totalDuplicates);
|
|
log.info("重复率: {:.4f}%", duplicationRate);
|
|
|
|
if (duplicationRate > 0.1) {
|
|
log.warn("警告:重复率超过0.1%,建议优化generateCode方法!");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 高并发多线程测试
|
|
*/
|
|
@Test
|
|
public void testHighConcurrencyGeneration() throws InterruptedException {
|
|
log.info("=== 开始高并发多线程测试 ===");
|
|
|
|
int threadCount = 20; // 20个并发线程
|
|
int codesPerThread = 50; // 每个线程生成50个流水号
|
|
int totalExpectedCodes = threadCount * codesPerThread;
|
|
|
|
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
|
CountDownLatch latch = new CountDownLatch(threadCount);
|
|
ConcurrentHashMap<String, Integer> allCodes = new ConcurrentHashMap<>();
|
|
List<String> allCodesList = Collections.synchronizedList(new ArrayList<>());
|
|
|
|
// 启动所有线程
|
|
for (int i = 0; i < threadCount; i++) {
|
|
final int threadId = i;
|
|
executor.submit(() -> {
|
|
try {
|
|
List<String> threadCodes = new ArrayList<>();
|
|
|
|
// 每个线程快速生成流水号
|
|
for (int j = 0; j < codesPerThread; j++) {
|
|
String code = generateCode();
|
|
threadCodes.add(code);
|
|
allCodesList.add(code);
|
|
|
|
// 统计重复
|
|
Integer count = allCodes.put(code, 1);
|
|
if (count != null) {
|
|
allCodes.put(code, count + 1);
|
|
}
|
|
}
|
|
|
|
log.debug("线程{}完成,生成{}个流水号", threadId, threadCodes.size());
|
|
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 等待所有线程完成
|
|
boolean finished = latch.await(30, TimeUnit.SECONDS);
|
|
executor.shutdown();
|
|
|
|
if (!finished) {
|
|
log.error("测试超时!");
|
|
return;
|
|
}
|
|
|
|
// 分析结果
|
|
Set<String> uniqueCodes = new HashSet<>(allCodesList);
|
|
int duplicates = totalExpectedCodes - uniqueCodes.size();
|
|
double duplicationRate = (double) duplicates / totalExpectedCodes * 100;
|
|
|
|
// 找出重复的流水号
|
|
List<Map.Entry<String, Integer>> duplicatedCodes = allCodes.entrySet().stream()
|
|
.filter(entry -> entry.getValue() > 1)
|
|
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
|
|
.limit(20) // 只显示前20个最多重复的
|
|
.toList();
|
|
|
|
log.info("=== 高并发测试结果 ===");
|
|
log.info("并发线程数: {}", threadCount);
|
|
log.info("每线程生成数: {}", codesPerThread);
|
|
log.info("预期总数: {}", totalExpectedCodes);
|
|
log.info("实际总数: {}", allCodesList.size());
|
|
log.info("唯一流水号数: {}", uniqueCodes.size());
|
|
log.info("重复数: {}", duplicates);
|
|
log.info("重复率: {:.4f}%", duplicationRate);
|
|
|
|
if (!duplicatedCodes.isEmpty()) {
|
|
log.warn("=== 发现重复流水号 ===");
|
|
duplicatedCodes.forEach(entry ->
|
|
log.warn("流水号: {} 重复了 {} 次", entry.getKey(), entry.getValue()));
|
|
}
|
|
|
|
if (duplicationRate > 1.0) {
|
|
log.error("严重警告:高并发下重复率超过1.0%,必须优化generateCode方法!");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 模拟极端高压场景:短时间内大量请求
|
|
*/
|
|
@Test
|
|
public void testExtremeHighPressure() throws InterruptedException {
|
|
log.info("=== 开始极端高压测试 ===");
|
|
|
|
int threadCount = 50; // 50个并发线程
|
|
int codesPerThread = 20; // 每个线程生成20个
|
|
long timeWindowMs = 1000; // 在1秒内完成
|
|
|
|
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
|
CountDownLatch startLatch = new CountDownLatch(1);
|
|
CountDownLatch endLatch = new CountDownLatch(threadCount);
|
|
|
|
ConcurrentHashMap<String, List<Integer>> codeToThreads = new ConcurrentHashMap<>();
|
|
List<String> allCodes = Collections.synchronizedList(new ArrayList<>());
|
|
|
|
// 准备所有线程
|
|
for (int i = 0; i < threadCount; i++) {
|
|
final int threadId = i;
|
|
executor.submit(() -> {
|
|
try {
|
|
// 等待开始信号
|
|
startLatch.await();
|
|
|
|
long startTime = System.currentTimeMillis();
|
|
List<String> threadCodes = new ArrayList<>();
|
|
|
|
// 在时间窗口内尽可能快地生成
|
|
while (System.currentTimeMillis() - startTime < timeWindowMs &&
|
|
threadCodes.size() < codesPerThread) {
|
|
String code = generateCode();
|
|
threadCodes.add(code);
|
|
allCodes.add(code);
|
|
|
|
// 记录哪个线程生成了这个流水号
|
|
codeToThreads.computeIfAbsent(code, k ->
|
|
Collections.synchronizedList(new ArrayList<>())).add(threadId);
|
|
}
|
|
|
|
log.debug("线程{}在{}ms内生成{}个流水号",
|
|
threadId, System.currentTimeMillis() - startTime, threadCodes.size());
|
|
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
} finally {
|
|
endLatch.countDown();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 开始测试
|
|
long testStartTime = System.currentTimeMillis();
|
|
startLatch.countDown();
|
|
|
|
// 等待完成
|
|
boolean finished = endLatch.await(timeWindowMs + 5000, TimeUnit.MILLISECONDS);
|
|
executor.shutdown();
|
|
long testDuration = System.currentTimeMillis() - testStartTime;
|
|
|
|
if (!finished) {
|
|
log.error("极端高压测试超时!");
|
|
return;
|
|
}
|
|
|
|
// 分析结果
|
|
Set<String> uniqueCodes = new HashSet<>(allCodes);
|
|
int totalGenerated = allCodes.size();
|
|
int duplicates = totalGenerated - uniqueCodes.size();
|
|
double duplicationRate = (double) duplicates / totalGenerated * 100;
|
|
double generationRate = (double) totalGenerated / testDuration * 1000; // 每秒生成数
|
|
|
|
// 分析重复模式
|
|
Map<String, List<Integer>> duplicatedCodes = codeToThreads.entrySet().stream()
|
|
.filter(entry -> entry.getValue().size() > 1)
|
|
.collect(Collectors.toMap(
|
|
Map.Entry::getKey,
|
|
Map.Entry::getValue
|
|
));
|
|
|
|
log.info("=== 极端高压测试结果 ===");
|
|
log.info("并发线程数: {}", threadCount);
|
|
log.info("预期每线程生成数: {}", codesPerThread);
|
|
log.info("测试持续时间: {}ms", testDuration);
|
|
log.info("实际总生成数: {}", totalGenerated);
|
|
log.info("唯一流水号数: {}", uniqueCodes.size());
|
|
log.info("重复数: {}", duplicates);
|
|
log.info("重复率: {:.4f}%", duplicationRate);
|
|
log.info("生成速率: {:.1f} codes/sec", generationRate);
|
|
|
|
if (!duplicatedCodes.isEmpty()) {
|
|
log.warn("=== 极端高压下的重复情况 ===");
|
|
duplicatedCodes.entrySet().stream()
|
|
.limit(10) // 只显示前10个
|
|
.forEach(entry -> {
|
|
String code = entry.getKey();
|
|
List<Integer> threads = entry.getValue();
|
|
log.warn("流水号: {} 被线程 {} 重复生成", code, threads);
|
|
});
|
|
}
|
|
|
|
// 评估结果
|
|
if (duplicationRate > 5.0) {
|
|
log.error("极严重警告:极端高压下重复率超过5.0%,generateCode方法不适合高并发场景!");
|
|
} else if (duplicationRate > 1.0) {
|
|
log.warn("警告:极端高压下重复率超过1.0%,建议优化generateCode方法");
|
|
}
|
|
|
|
if (generationRate > 10000) {
|
|
log.info("性能良好:生成速率超过10,000 codes/sec");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 综合测试报告
|
|
*/
|
|
@Test
|
|
public void generateComprehensiveReport() {
|
|
log.info("=== 生成综合测试报告 ===");
|
|
|
|
// 基础性能测试
|
|
long startTime = System.nanoTime();
|
|
List<String> sample = new ArrayList<>();
|
|
for (int i = 0; i < 1000; i++) {
|
|
sample.add(generateCode());
|
|
}
|
|
long duration = System.nanoTime() - startTime;
|
|
double avgTimePerCode = duration / 1000.0 / 1_000_000; // 毫秒
|
|
|
|
// 唯一性分析
|
|
Set<String> uniqueSample = new HashSet<>(sample);
|
|
double sampleDuplicationRate = (double) (sample.size() - uniqueSample.size()) / sample.size() * 100;
|
|
|
|
// 长度和格式分析
|
|
String sampleCode = generateCode();
|
|
int codeLength = sampleCode.length();
|
|
boolean hasCorrectPrefix = sampleCode.startsWith(CODE_PREFIX);
|
|
|
|
// 理论分析
|
|
double theoreticalCollisionProbability = calculateBirthdayParadoxProbability(10, 100000);
|
|
|
|
log.info("=== generateCode方法综合评估报告 ===");
|
|
log.info("基础信息:");
|
|
log.info(" - 代码前缀: {}", CODE_PREFIX);
|
|
log.info(" - 流水号长度: {}", codeLength);
|
|
log.info(" - 格式正确: {}", hasCorrectPrefix);
|
|
log.info(" - 示例流水号: {}", sampleCode);
|
|
|
|
log.info("性能指标:");
|
|
log.info(" - 平均生成时间: {:.3f}ms", avgTimePerCode);
|
|
log.info(" - 理论最大生成速率: {:.0f} codes/sec", 1000.0 / avgTimePerCode);
|
|
|
|
log.info("唯一性分析:");
|
|
log.info(" - 样本重复率: {:.4f}% (1000个样本)", sampleDuplicationRate);
|
|
log.info(" - 理论冲突概率: {:.4f}% (1秒内10个)", theoreticalCollisionProbability * 100);
|
|
log.info(" - 随机数范围: 100,000 (00000-99999)");
|
|
|
|
log.info("风险评估:");
|
|
if (sampleDuplicationRate > 0.5) {
|
|
log.error(" - 高风险:样本重复率过高,不适合生产环境");
|
|
} else if (sampleDuplicationRate > 0.1) {
|
|
log.warn(" - 中风险:存在一定重复概率,建议优化");
|
|
} else {
|
|
log.info(" - 低风险:重复概率较低,基本可用");
|
|
}
|
|
|
|
log.info("优化建议:");
|
|
log.info(" - 建议1:使用毫秒级时间戳替代秒级");
|
|
log.info(" - 建议2:增加机器标识或进程ID");
|
|
log.info(" - 建议3:使用原子递增计数器");
|
|
log.info(" - 建议4:采用UUID算法确保全局唯一性");
|
|
}
|
|
|
|
/**
|
|
* 计算生日悖论概率
|
|
*/
|
|
private double calculateBirthdayParadoxProbability(int n, int d) {
|
|
if (n > d) return 1.0;
|
|
|
|
double probability = 1.0;
|
|
for (int i = 0; i < n; i++) {
|
|
probability *= (double) (d - i) / d;
|
|
}
|
|
return 1.0 - probability;
|
|
}
|
|
} |