在短视频内容爆炸式增长的今天,去水印工具已成为用户刚需。基于uni-app开发去水印小程序,能够实现“一次开发,多端发布”,显著降低开发成本。本文将详细解析一个完整去水印小程序的开发全流程,涵盖从环境搭建到最终上线的各个环节。
一、技术选型
1.1技术栈选择
•源码及演示:y.wxlbyx.icu
•前端框架:uni-app 3.5+(Vue 3+Composition API)
•UI组件库:uni-ui
•网络请求:uni.request封装
•视频处理:后端API+前端预览
•存储方案:uni.setStorageSync本地缓存
•平台支持:微信小程序、抖音小程序、H5
1.2项目结构设计
┌─────────────────────────────────────────────────┐ │ 客户端 │ │ (Web/Android/iOS) │ └───────────────┬─────────────────────────────────┘ │ HTTP/WebSocket ┌───────────────┴─────────────────────────────────┐ │ API网关层 │ │ (Spring Cloud Gateway) │ └───────────────┬─────────────────────────────────┘ │ ┌───────────────┴─────────────────────────────────┐ │ 微服务集群 │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │用户服务 │ │盲盒服务 │ │聊天服务 │ │ │ └─────────┘ └─────────┘ └─────────┘ │ └───────────────┬─────────────────────────────────┘ │ ┌───────────────┴─────────────────────────────────┐ │ 数据存储层 │ │ MySQL Redis MinIO │ └─────────────────────────────────────────────────┘
二、开发环境搭建
2.1开发工具准备
-- 用户表 CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password_hash` varchar(255) NOT NULL COMMENT '加密密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `gender` tinyint DEFAULT '0' COMMENT '性别 0:未知 1:男 2:女', `age` tinyint DEFAULT NULL COMMENT '年龄', `avatar_url` varchar(500) DEFAULT NULL COMMENT '头像URL', `introduction` text COMMENT '个人介绍', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `status` tinyint DEFAULT '1' COMMENT '状态 0:禁用 1:正常', `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), KEY `idx_phone` (`phone`), KEY `idx_email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 盲盒配置表 CREATE TABLE `blind_box_config` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '配置ID', `name` varchar(100) NOT NULL COMMENT '盲盒名称', `description` text COMMENT '盲盒描述', `price` decimal(10,2) NOT NULL COMMENT '价格', `daily_limit` int DEFAULT '-1' COMMENT '每日限制 -1:无限制', `total_limit` int DEFAULT '-1' COMMENT '总限制 -1:无限制', `match_algorithm` varchar(50) DEFAULT 'RANDOM' COMMENT '匹配算法', `tags` json DEFAULT NULL COMMENT '标签配置', `status` tinyint DEFAULT '1' COMMENT '状态 0:下架 1:上架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='盲盒配置表'; -- 用户盲盒记录表 CREATE TABLE `user_box_record` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '记录ID', `user_id` bigint NOT NULL COMMENT '用户ID', `box_id` bigint NOT NULL COMMENT '盲盒ID', `order_no` varchar(64) NOT NULL COMMENT '订单号', `match_user_id` bigint DEFAULT NULL COMMENT '匹配用户ID', `match_status` tinyint DEFAULT '0' COMMENT '匹配状态 0:等待匹配 1:匹配成功 2:匹配失败', `match_time` datetime DEFAULT NULL COMMENT '匹配时间', `unlock_time` datetime DEFAULT NULL COMMENT '解锁时间', `is_unlocked` tinyint DEFAULT '0' COMMENT '是否已解锁', `user_rating` tinyint DEFAULT NULL COMMENT '用户评分 1-5', `match_rating` tinyint DEFAULT NULL COMMENT '对方评分 1-5', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_match_user_id` (`match_user_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户盲盒记录表'; -- 聊天会话表 CREATE TABLE `chat_session` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '会话ID', `session_key` varchar(100) NOT NULL COMMENT '会话唯一标识', `user1_id` bigint NOT NULL COMMENT '用户1ID', `user2_id` bigint NOT NULL COMMENT '用户2ID', `box_record_id` bigint NOT NULL COMMENT '盲盒记录ID', `last_message` text COMMENT '最后消息', `last_message_time` datetime DEFAULT NULL COMMENT '最后消息时间', `unread_count1` int DEFAULT '0' COMMENT '用户1未读', `unread_count2` int DEFAULT '0' COMMENT '用户2未读', `status` tinyint DEFAULT '1' COMMENT '状态 1:正常 0:关闭', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_session_key` (`session_key`), KEY `idx_user1_id` (`user1_id`,`update_time`), KEY `idx_user2_id` (`user2_id`,`update_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天会话表'; -- 消息表 CREATE TABLE `chat_message` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '消息ID', `session_id` bigint NOT NULL COMMENT '会话ID', `sender_id` bigint NOT NULL COMMENT '发送者ID', `receiver_id` bigint NOT NULL COMMENT '接收者ID', `message_type` tinyint DEFAULT '1' COMMENT '消息类型 1:文本 2:图片 3:语音', `content` text COMMENT '消息内容', `file_url` varchar(500) DEFAULT NULL COMMENT '文件URL', `is_read` tinyint DEFAULT '0' COMMENT '是否已读', `read_time` datetime DEFAULT NULL COMMENT '阅读时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_session_id` (`session_id`,`create_time`), KEY `idx_sender_id` (`sender_id`,`create_time`), KEY `idx_receiver_id` (`receiver_id`,`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表'; -- 用户标签表 CREATE TABLE `user_tag` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '标签ID', `user_id` bigint NOT NULL COMMENT '用户ID', `tag_name` varchar(50) NOT NULL COMMENT '标签名称', `tag_type` varchar(20) DEFAULT 'CUSTOM' COMMENT '标签类型', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_tag` (`user_id`,`tag_name`), KEY `idx_tag_name` (`tag_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户标签表'; -- 匹配记录详情表 CREATE TABLE `match_detail` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '详情ID', `record_id` bigint NOT NULL COMMENT '记录ID', `user_id` bigint NOT NULL COMMENT '用户ID', `match_strategy` varchar(50) DEFAULT NULL COMMENT '匹配策略', `match_score` decimal(5,2) DEFAULT NULL COMMENT '匹配分数', `match_factors` json DEFAULT NULL COMMENT '匹配因素', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_record_id` (`record_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='匹配记录详情表';
三、核心功能实现
3.1应用配置与初始化
// JWT工具类
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
// Spring Security配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/ws/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
}
3.2首页功能实现
@Service
@Slf4j
public class BlindBoxServiceImpl implements BlindBoxService {
@Autowired
private UserBoxRecordMapper boxRecordMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private BlindBoxConfigMapper boxConfigMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String MATCH_QUEUE = "blindbox.match.queue";
private static final String MATCH_POOL_KEY = "match:pool:";
@Override
@Transactional
public ApiResult<BoxOpenResult> openBlindBox(Long userId, Long boxId) {
// 1. 参数校验
User user = userMapper.selectById(userId);
if (user == null) {
return ApiResult.error("用户不存在");
}
BlindBoxConfig config = boxConfigMapper.selectById(boxId);
if (config == null || config.getStatus() != 1) {
return ApiResult.error("盲盒不存在或已下架");
}
// 2. 检查购买限制
if (!checkPurchaseLimit(userId, boxId, config)) {
return ApiResult.error("已达到购买限制");
}
// 3. 创建盲盒记录
UserBoxRecord record = new UserBoxRecord();
record.setUserId(userId);
record.setBoxId(boxId);
record.setOrderNo(generateOrderNo());
record.setMatchStatus(0);
record.setIsUnlocked(false);
boxRecordMapper.insert(record);
// 4. 发送到匹配队列
MatchTask task = new MatchTask();
task.setRecordId(record.getId());
task.setUserId(userId);
task.setBoxId(boxId);
task.setConfig(config);
rabbitTemplate.convertAndSend(MATCH_QUEUE, task);
// 5. 将用户加入匹配池
addToMatchPool(userId, boxId, record.getId(), config);
return ApiResult.success(new BoxOpenResult(record.getId(), "等待匹配中"));
}
// 智能匹配算法
private MatchResult smartMatch(Long userId, Long boxId,
BlindBoxConfig config) {
List<Long> candidateUserIds = getMatchPoolUsers(boxId);
if (candidateUserIds.isEmpty()) {
return null;
}
// 获取当前用户信息
User currentUser = userMapper.selectById(userId);
Map<String, Object> currentUserTags = getUserTags(userId);
// 计算匹配度
Map<Long, Double> matchScores = new HashMap<>();
for (Long candidateId : candidateUserIds) {
if (candidateId.equals(userId)) {
continue;
}
User candidate = userMapper.selectById(candidateId);
Map<String, Object> candidateTags = getUserTags(candidateId);
double score = calculateMatchScore(currentUser, candidate,
currentUserTags, candidateTags,
config);
matchScores.put(candidateId, score);
}
// 选择最佳匹配
return matchScores.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(entry -> new MatchResult(entry.getKey(), entry.getValue()))
.orElse(null);
}
private double calculateMatchScore(User user1, User user2,
Map<String, Object> tags1,
Map<String, Object> tags2,
BlindBoxConfig config) {
double score = 0.0;
// 1. 基础信息匹配(年龄、性别等)
if (config.getGenderPreference() != null) {
if (config.getGenderPreference().equals("SAME") &&
user1.getGender().equals(user2.getGender())) {
score += 30;
} else if (config.getGenderPreference().equals("DIFFERENT") &&
!user1.getGender().equals(user2.getGender())) {
score += 30;
}
}
// 2. 年龄差计算
int ageDiff = Math.abs(user1.getAge() - user2.getAge());
if (ageDiff <= 3) {
score += 20;
} else if (ageDiff <= 5) {
score += 10;
}
// 3. 标签匹配度
Set<String> commonTags = new HashSet<>(tags1.keySet());
commonTags.retainAll(tags2.keySet());
score += commonTags.size() * 5;
// 4. 活跃度因素
Double user1Activity = getUserActivityScore(user1.getId());
Double user2Activity = getUserActivityScore(user2.getId());
score += Math.min(user1Activity, user2Activity) * 10;
return score;
}
// 消息队列消费者
@RabbitListener(queues = MATCH_QUEUE)
public void processMatchTask(MatchTask task) {
log.info("开始处理匹配任务: {}", task.getRecordId());
try {
// 尝试匹配
MatchResult matchResult = smartMatch(task.getUserId(),
task.getBoxId(), task.getConfig());
if (matchResult != null && matchResult.getScore() > 50) {
// 匹配成功
completeMatch(task.getRecordId(), matchResult.getMatchedUserId());
} else {
// 匹配失败,加入重试队列
if (task.getRetryCount() < 3) {
task.setRetryCount(task.getRetryCount() + 1);
rabbitTemplate.convertAndSend(MATCH_QUEUE, task, message -> {
message.getMessageProperties()
.setDelay(30000); // 30秒后重试
return message;
});
} else {
// 匹配失败,退款处理
handleMatchFailed(task.getRecordId());
}
}
} catch (Exception e) {
log.error("匹配处理异常: ", e);
}
}
private void completeMatch(Long recordId, Long matchedUserId) {
UserBoxRecord record = boxRecordMapper.selectById(recordId);
if (record == null || record.getMatchStatus() != 0) {
return;
}
// 更新匹配记录
record.setMatchUserId(matchedUserId);
record.setMatchStatus(1);
record.setMatchTime(new Date());
boxRecordMapper.updateById(record);
// 创建聊天会话
createChatSession(record.getUserId(), matchedUserId, recordId);
// 发送匹配成功通知
sendMatchNotification(record.getUserId(), matchedUserId);
}
}
3.3视频解析API封装
@Component
@ServerEndpoint("/api/ws/chat/{token}")
@Slf4j
public class ChatWebSocket {
private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>();
private static final Map<String, String> userSessions = new ConcurrentHashMap<>();
@Autowired
private ChatService chatService;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@OnOpen
public void onOpen(@PathParam("token") String token, Session session) {
try {
String username = jwtTokenProvider.getUsernameFromToken(token);
onlineUsers.put(username, session);
userSessions.put(session.getId(), username);
log.info("用户连接: {}, 当前在线: {}", username, onlineUsers.size());
// 发送连接成功消息
sendMessage(session, new WebSocketMessage(
"SYSTEM", "CONNECT_SUCCESS", "连接成功"
));
} catch (Exception e) {
log.error("WebSocket连接失败: ", e);
try {
session.close();
} catch (IOException ex) {
log.error("关闭连接失败: ", ex);
}
}
}
@OnMessage
public void onMessage(String messageStr, Session session) {
try {
WebSocketMessage message = JSON.parseObject(messageStr,
WebSocketMessage.class);
String username = userSessions.get(session.getId());
if (username == null) {
return;
}
switch (message.getType()) {
case "CHAT":
handleChatMessage(username, message);
break;
case "TYPING":
handleTypingStatus(username, message);
break;
case "READ":
handleReadReceipt(username, message);
break;
}
} catch (Exception e) {
log.error("处理消息异常: ", e);
}
}
private void handleChatMessage(String sender, WebSocketMessage message) {
ChatMessage chatMessage = JSON.parseObject(
message.getData().toString(), ChatMessage.class);
// 保存到数据库
Long messageId = chatService.saveMessage(chatMessage);
chatMessage.setId(messageId);
// 更新会话最后消息
chatService.updateSessionLastMessage(
chatMessage.getSessionId(),
chatMessage.getContent(),
new Date()
);
// 发送给接收者
String receiver = chatMessage.getReceiverId().toString();
Session receiverSession = onlineUsers.get(receiver);
if (receiverSession != null && receiverSession.isOpen()) {
sendMessage(receiverSession, new WebSocketMessage(
"CHAT", "NEW_MESSAGE", chatMessage
));
}
// 发送回执给发送者
WebSocketMessage receipt = new WebSocketMessage(
"SYSTEM", "MESSAGE_SENT",
Map.of("messageId", messageId, "timestamp", new Date())
);
Session senderSession = onlineUsers.get(sender);
if (senderSession != null) {
sendMessage(senderSession, receipt);
}
}
@OnClose
public void onClose(Session session) {
String username = userSessions.remove(session.getId());
if (username != null) {
onlineUsers.remove(username);
log.info("用户断开: {}, 当前在线: {}", username, onlineUsers.size());
}
}
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocket错误: ", error);
}
private void sendMessage(Session session, WebSocketMessage message) {
try {
if (session.isOpen()) {
session.getBasicRemote().sendText(JSON.toJSONString(message));
}
} catch (IOException e) {
log.error("发送消息失败: ", e);
}
}
// 广播消息
public static void broadcast(String messageType, Object data) {
WebSocketMessage message = new WebSocketMessage(
"SYSTEM", messageType, data
);
String messageJson = JSON.toJSONString(message);
onlineUsers.values().forEach(session -> {
if (session.isOpen()) {
try {
session.getBasicRemote().sendText(messageJson);
} catch (IOException e) {
log.error("广播消息失败: ", e);
}
}
});
}
}
// WebSocket配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private ChatWebSocket chatWebSocket;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatWebSocket, "/api/ws/chat/{token}")
.setAllowedOrigins("*")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.4历史记录功能
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserBoxRecordMapper boxRecordMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
@Transactional
public ApiResult<OrderVO> createOrder(Long userId, Long boxId,
Integer quantity) {
// 1. 验证盲盒
BlindBoxConfig config = boxConfigMapper.selectById(boxId);
if (config == null || config.getStatus() != 1) {
return ApiResult.error("盲盒不存在或已下架");
}
// 2. 创建订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(userId);
order.setBoxId(boxId);
order.setQuantity(quantity);
order.setUnitPrice(config.getPrice());
order.setTotalPrice(config.getPrice().multiply(
new BigDecimal(quantity)));
order.setStatus(0); // 待支付
order.setCreateTime(new Date());
orderMapper.insert(order);
// 3. 调用支付接口
PaymentRequest paymentRequest = new PaymentRequest();
paymentRequest.setOrderNo(order.getOrderNo());
paymentRequest.setAmount(order.getTotalPrice());
paymentRequest.setUserId(userId);
paymentRequest.setSubject("交友盲盒购买");
PaymentResponse paymentResponse = paymentService.createPayment(
paymentRequest);
// 4. 返回支付信息
OrderVO orderVO = convertToVO(order);
orderVO.setPayInfo(paymentResponse.getPayInfo());
return ApiResult.success(orderVO);
}
// 支付回调处理
@PostMapping("/payment/callback")
public String paymentCallback(@RequestBody PaymentCallback callback) {
log.info("收到支付回调: {}", callback);
// 验证签名
if (!verifySignature(callback)) {
return "FAIL";
}
if ("SUCCESS".equals(callback.getTradeStatus())) {
// 支付成功处理
handlePaymentSuccess(callback.getOrderNo());
} else {
// 支付失败处理
handlePaymentFailed(callback.getOrderNo());
}
return "SUCCESS";
}
@Async
protected void handlePaymentSuccess(String orderNo) {
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
log.error("订单不存在: {}", orderNo);
return;
}
// 更新订单状态
order.setStatus(1); // 已支付
order.setPayTime(new Date());
orderMapper.updateById(order);
// 创建盲盒记录
for (int i = 0; i < order.getQuantity(); i++) {
UserBoxRecord record = new UserBoxRecord();
record.setUserId(order.getUserId());
record.setBoxId(order.getBoxId());
record.setOrderNo(order.getOrderNo());
record.setMatchStatus(0);
record.setIsUnlocked(false);
boxRecordMapper.insert(record);
// 发送到匹配队列
MatchTask task = new MatchTask();
task.setRecordId(record.getId());
task.setUserId(order.getUserId());
task.setBoxId(order.getBoxId());
rabbitTemplate.convertAndSend("blindbox.match.queue", task);
}
// 发送支付成功通知
notificationService.sendPaymentSuccessNotification(
order.getUserId(), order);
}
}
四、多平台适配
4.1条件编译处理
@Service
@Slf4j
public class UserCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
private static final String USER_CACHE_KEY = "user:info:";
private static final long USER_CACHE_TTL = 3600; // 1小时
@Cacheable(value = "user", key = "#userId", unless = "#result == null")
public User getUserById(Long userId) {
String cacheKey = USER_CACHE_KEY + userId;
// 从缓存读取
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user;
}
// 从数据库读取
user = userMapper.selectById(userId);
if (user != null) {
// 写入缓存
redisTemplate.opsForValue().set(
cacheKey, user, USER_CACHE_TTL, TimeUnit.SECONDS);
}
return user;
}
@CacheEvict(value = "user", key = "#userId")
public void evictUserCache(Long userId) {
String cacheKey = USER_CACHE_KEY + userId;
redisTemplate.delete(cacheKey);
}
// 批量获取用户信息(缓存优化)
public Map<Long, User> batchGetUsers(List<Long> userIds) {
Map<Long, User> result = new HashMap<>();
List<Long> missingIds = new ArrayList<>();
// 先从缓存获取
for (Long userId : userIds) {
String cacheKey = USER_CACHE_KEY + userId;
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
result.put(userId, user);
} else {
missingIds.add(userId);
}
}
// 从数据库获取缺失的数据
if (!missingIds.isEmpty()) {
List<User> dbUsers = userMapper.selectBatchIds(missingIds);
for (User user : dbUsers) {
result.put(user.getId(), user);
// 写入缓存
String cacheKey = USER_CACHE_KEY + user.getId();
redisTemplate.opsForValue().set(
cacheKey, user, USER_CACHE_TTL, TimeUnit.SECONDS);
}
}
return result;
}
}
4.2 分布式锁实现
@Component
public class DistributedLock {
@Autowired
private RedissonClient redissonClient;
public <T> T executeWithLock(String lockKey, long waitTime,
long leaseTime, Supplier<T> supplier) {
RLock lock = redissonClient.getLock(lockKey);
boolean locked = false;
try {
locked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
if (locked) {
return supplier.get();
} else {
throw new BusinessException("获取锁失败,请稍后重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("获取锁被中断");
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
// 在盲盒购买中使用分布式锁
@Service
public class BlindBoxPurchaseService {
@Autowired
private DistributedLock distributedLock;
public ApiResult purchaseBox(Long userId, Long boxId) {
String lockKey = "purchase:lock:" + boxId + ":" + userId;
return distributedLock.executeWithLock(lockKey, 3, 10, () -> {
// 检查库存
Integer stock = getBoxStock(boxId);
if (stock <= 0) {
return ApiResult.error("库存不足");
}
// 检查用户购买限制
if (hasReachedDailyLimit(userId, boxId)) {
return ApiResult.error("今日购买次数已达上限");
}
// 执行购买逻辑
return doPurchase(userId, boxId);
});
}
}

五、性能优化
5.1图片懒加载优化
# Dockerfile
FROM openjdk:11-jre-slim
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", \
"-Djava.security.egd=file:/dev/./urandom", \
"-Dspring.profiles.active=${ACTIVE_PROFILE}", \
"/app.jar"]
# docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: blindbox
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2-alpine
ports:
- "6379:6379"
command: redis-server --appendonly yes
rabbitmq:
image: rabbitmq:3.9-management
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: admin123
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
DB_HOST: mysql
REDIS_HOST: redis
RABBITMQ_HOST: rabbitmq
depends_on:
- mysql
- redis
- rabbitmq
volumes:
mysql_data:
5.2请求缓存优化
// utils/cache.js
// 请求缓存管理
class RequestCache {
constructor() {
this.cache = new Map()
this.maxSize = 100
}
// 生成缓存key
generateKey(config) {
return `${config.method}_${config.url}_${JSON.stringify(config.data)}`
}
// 获取缓存
get(key) {
const item = this.cache.get(key)
if (!item) return null
// 检查过期
if (item.expire && Date.now() > item.expire) {
this.cache.delete(key)
return null
}
return item.data
}
// 设置缓存
set(key, data, expire = 300000) { // 默认5分钟
// 清理旧缓存
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, {
data,
expire: expire > 0 ? Date.now() + expire : 0
})
}
// 清除缓存
clear() {
this.cache.clear()
}
}
// 在请求中应用缓存
export const cachedRequest = async (config) => {
const cache = new RequestCache()
const key = cache.generateKey(config)
// 尝试从缓存获取
if (config.cache !== false) {
const cached = cache.get(key)
if (cached) {
return cached
}
}
// 发送请求
const result = await request(config)
// 缓存结果
if (config.cache !== false && result.success) {
cache.set(key, result, config.cacheTime || 300000)
}
return result
}
结语
通过本文的完整流程拆解,我们系统性地实现了基于uni-app的去水印小程序开发。从项目初始化、核心功能模块构建,到多平台适配优化,最后完成打包发布,形成了一个完整的开发闭环。整个系统采用Vue 3组合式API编写,代码结构清晰可维护,充分利用了uni-app的跨平台优势。关键实现包括:智能链接解析、高效视频处理、本地历史记录管理、用户设置存储等核心功能,并通过条件编译优雅处理了多平台差异。性能优化方面实现了图片懒包等关键技术,确保了小程序运行的流畅性。开发者可基于此源码快速构建自己的去水印应用,并根据实际需求扩展批量处理、视频编辑、会员服务等增值功能。这套解决方案不仅提供了完整的技术实现,更为后续产品迭代奠定了坚实基础。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70049145/viewspace-3106765/ ,如需转载,请注明出处,否则将追究法律责任。