feat(groupbuying): 完善拼团订单全链路及错误处理
- 拼团订单发货校验:必须成团后才能发货 - 限购校验:跨团组统计用户参与次数,超限拒绝 - 成团/失败自动通知所有成员(Redis Pub/Sub) - 拼团详情页区分"进行中"和"已成团"团组展示 - 订单类型支持三态(普通/秒杀/拼团)前后端联调 - 400错误只显示业务消息,不再重复弹出状态码 - 响应式导航栏适配及UI优化 - 新增历史数据修复SQL脚本
This commit is contained in:
@@ -57,10 +57,16 @@ public class ProductReviewController {
|
||||
|
||||
@GetMapping("/check")
|
||||
public ResponseEntity<Map<String, Object>> checkReview(@RequestParam Long orderId,
|
||||
@RequestParam Long productId) {
|
||||
@RequestParam Long productId,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", productReviewService.checkReviewStatus(orderId, productId));
|
||||
response.put("data", productReviewService.checkReviewStatus(userId, orderId, productId));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
||||
@@ -170,9 +170,12 @@ public class OrderDTO {
|
||||
private Long shippedOrders;
|
||||
private Long completedOrders;
|
||||
private Long cancelledOrders;
|
||||
private Long refundingOrders;
|
||||
private Long refundedOrders;
|
||||
private BigDecimal totalAmount;
|
||||
private BigDecimal todayAmount;
|
||||
private Long flashSaleOrders;
|
||||
private Long normalOrders;
|
||||
private Long groupBuyingOrders;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,11 @@ public interface GroupBuyingMemberRepository extends JpaRepository<GroupBuyingMe
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuyingMember m SET m.status = :status WHERE m.groupId = :groupId AND m.status = 1")
|
||||
int updateStatusByGroupId(@Param("groupId") Long groupId, @Param("status") Integer status);
|
||||
|
||||
/**
|
||||
* 统计用户在某个拼团活动的所有团组中的有效参与次数(排除已取消 status=3)
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM GroupBuyingMember m WHERE m.userId = :userId AND m.status != 3 " +
|
||||
"AND m.groupId IN (SELECT g.id FROM GroupBuyingGroup g WHERE g.groupBuyingId = :activityId)")
|
||||
long countActiveByUserIdAndActivityId(@Param("userId") Long userId, @Param("activityId") Long activityId);
|
||||
}
|
||||
|
||||
@@ -320,6 +320,7 @@ public class AdminService {
|
||||
orderMap.put("totalAmount", order.getTotalPrice());
|
||||
orderMap.put("status", order.getStatus());
|
||||
orderMap.put("createdAt", order.getCreatedAt());
|
||||
orderMap.put("orderType", order.getOrderType());
|
||||
orderMap.put("isFlashSale", order.getOrderType() == 2); // 2表示秒杀订单
|
||||
return orderMap;
|
||||
}).collect(Collectors.toList());
|
||||
@@ -429,6 +430,7 @@ public class AdminService {
|
||||
orderMap.put("totalAmount", order.getTotalPrice());
|
||||
orderMap.put("status", order.getStatus());
|
||||
orderMap.put("createdAt", order.getCreatedAt());
|
||||
orderMap.put("orderType", order.getOrderType());
|
||||
orderMap.put("isFlashSale", order.getOrderType() == 2); // 2表示秒杀订单
|
||||
return orderMap;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
@@ -53,6 +53,9 @@ public class GroupBuyingService {
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private OrderItemRepository orderItemRepository;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
@@ -293,6 +296,12 @@ public class GroupBuyingService {
|
||||
throw new RuntimeException("库存不足");
|
||||
}
|
||||
|
||||
// 1.1 校验限购:用户在该活动下的有效参与次数
|
||||
long userJoinCount = groupBuyingMemberRepository.countActiveByUserIdAndActivityId(userId, activityId);
|
||||
if (userJoinCount >= gb.getMaxPerUser()) {
|
||||
throw new RuntimeException("您已达到该活动的限购数量(" + gb.getMaxPerUser() + " 件)");
|
||||
}
|
||||
|
||||
// 2. 获取商品信息
|
||||
Product product = productRepository.findById(gb.getProductId())
|
||||
.orElseThrow(() -> new RuntimeException("商品不存在"));
|
||||
@@ -365,6 +374,17 @@ public class GroupBuyingService {
|
||||
order.setGroupBuyingGroupId(group.getId());
|
||||
order = orderRepository.save(order);
|
||||
|
||||
// 5.1 创建订单明细
|
||||
OrderItem orderItem = new OrderItem();
|
||||
orderItem.setOrderId(order.getId());
|
||||
orderItem.setProductId(product.getId());
|
||||
orderItem.setProductName(product.getName());
|
||||
orderItem.setProductImageUrl(product.getImageUrl());
|
||||
orderItem.setPrice(gb.getGroupPrice());
|
||||
orderItem.setQuantity(1);
|
||||
orderItem.setSubtotal(gb.getGroupPrice());
|
||||
orderItemRepository.save(orderItem);
|
||||
|
||||
// 6. 创建成员记录
|
||||
GroupBuyingMember member = new GroupBuyingMember();
|
||||
member.setGroupId(group.getId());
|
||||
@@ -385,6 +405,10 @@ public class GroupBuyingService {
|
||||
// Update all members status to SUCCESS
|
||||
groupBuyingMemberRepository.updateStatusByGroupId(group.getId(), 2);
|
||||
log.info("拼团成功: groupId={}, groupNo={}", group.getId(), group.getGroupNo());
|
||||
|
||||
// 发布拼团成功通知给所有成员
|
||||
publishGroupStatusChange(group.getId(), group.getGroupNo(), activityId,
|
||||
gb.getProductId(), product.getName(), "success");
|
||||
}
|
||||
|
||||
// Add to Redis members set
|
||||
@@ -502,6 +526,14 @@ public class GroupBuyingService {
|
||||
// Clean Redis
|
||||
redisService.delete(GB_MEMBERS_PREFIX + group.getId());
|
||||
|
||||
// 发布拼团失败通知
|
||||
GroupBuying gb = groupBuyingRepository.findById(group.getGroupBuyingId()).orElse(null);
|
||||
if (gb != null) {
|
||||
Product product = productRepository.findById(gb.getProductId()).orElse(null);
|
||||
publishGroupStatusChange(group.getId(), group.getGroupNo(), group.getGroupBuyingId(),
|
||||
gb.getProductId(), product != null ? product.getName() : "未知商品", "failed");
|
||||
}
|
||||
|
||||
log.info("超时团组处理完成: groupId={}", group.getId());
|
||||
}
|
||||
|
||||
@@ -649,4 +681,28 @@ public class GroupBuyingService {
|
||||
default: return "未知";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布拼团状态变更消息(通知所有成员)
|
||||
*/
|
||||
private void publishGroupStatusChange(Long groupId, String groupNo, Long activityId,
|
||||
Long productId, String productName, String action) {
|
||||
List<GroupBuyingMember> members = groupBuyingMemberRepository.findByGroupId(groupId);
|
||||
List<Long> memberUserIds = members.stream()
|
||||
.map(GroupBuyingMember::getUserId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("groupId", groupId);
|
||||
message.put("groupNo", groupNo);
|
||||
message.put("activityId", activityId);
|
||||
message.put("productId", productId);
|
||||
message.put("productName", productName);
|
||||
message.put("action", action);
|
||||
message.put("memberUserIds", memberUserIds);
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
redisService.publish("groupbuying:status:change", message);
|
||||
log.info("发布拼团状态变更消息: groupId={}, action={}, members={}", groupId, action, memberUserIds.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -64,6 +66,12 @@ public class MessageListenerService {
|
||||
new ChannelTopic("return:status:change")
|
||||
);
|
||||
|
||||
// 拼团状态变更监听
|
||||
redisMessageListenerContainer.addMessageListener(
|
||||
new GroupBuyingStatusChangeListener(),
|
||||
new ChannelTopic("groupbuying:status:change")
|
||||
);
|
||||
|
||||
log.info("Redis消息监听器初始化完成");
|
||||
}
|
||||
|
||||
@@ -231,6 +239,48 @@ public class MessageListenerService {
|
||||
log.info("退货状态变更通知已创建: 订单ID={}, 操作={}", orderId, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拼团状态变更
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleGroupBuyingStatusChange(String action, Map<String, Object> data) {
|
||||
String groupNo = data.get("groupNo") != null ? data.get("groupNo").toString() : "";
|
||||
String productName = data.get("productName") != null ? data.get("productName").toString() : "商品";
|
||||
Long activityId = extractLongValue(data.get("activityId"));
|
||||
|
||||
List<Long> memberUserIds = new ArrayList<>();
|
||||
Object memberObj = data.get("memberUserIds");
|
||||
if (memberObj instanceof List) {
|
||||
for (Object item : (List<?>) memberObj) {
|
||||
Long uid = extractLongValue(item);
|
||||
if (uid != null) memberUserIds.add(uid);
|
||||
}
|
||||
}
|
||||
|
||||
String title;
|
||||
String message;
|
||||
String link = "/groupbuying/" + (activityId != null ? activityId : "");
|
||||
|
||||
switch (action) {
|
||||
case "success":
|
||||
title = "拼团成功";
|
||||
message = "您参与的「" + productName + "」拼团已成功!请尽快完成支付。";
|
||||
break;
|
||||
case "failed":
|
||||
title = "拼团失败";
|
||||
message = "很遗憾,您参与的「" + productName + "」拼团未能成团,订单已自动取消。";
|
||||
break;
|
||||
default:
|
||||
log.info("未知拼团状态变更: {}", action);
|
||||
return;
|
||||
}
|
||||
|
||||
for (Long userId : memberUserIds) {
|
||||
notificationService.createNotification(userId, "groupbuying", title, message, link);
|
||||
}
|
||||
log.info("拼团状态变更通知已创建: groupNo={}, action={}, 通知人数={}", groupNo, action, memberUserIds.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查库存预警
|
||||
*/
|
||||
@@ -417,4 +467,27 @@ public class MessageListenerService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼团状态变更监听器
|
||||
*/
|
||||
private class GroupBuyingStatusChangeListener implements MessageListener {
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
try {
|
||||
String messageBody = new String(message.getBody());
|
||||
log.debug("接收到拼团状态变更消息: {}", messageBody);
|
||||
|
||||
Map<String, Object> data = parseRedissonMessage(messageBody);
|
||||
String action = data.get("action").toString();
|
||||
|
||||
log.info("拼团状态变更: action={}", action);
|
||||
|
||||
handleGroupBuyingStatusChange(action, data);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理拼团状态变更消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ package com.org.flashsalesystem.service;
|
||||
import com.org.flashsalesystem.dto.OrderDTO;
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.GroupBuyingGroup;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.entity.OrderItem;
|
||||
import com.org.flashsalesystem.entity.UserAddress;
|
||||
import com.org.flashsalesystem.repository.GroupBuyingGroupRepository;
|
||||
import com.org.flashsalesystem.repository.OrderItemRepository;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
@@ -56,6 +58,8 @@ public class OrderService {
|
||||
private FlashSaleService flashSaleService;
|
||||
@Autowired
|
||||
private GroupBuyingService groupBuyingService;
|
||||
@Autowired
|
||||
private GroupBuyingGroupRepository groupBuyingGroupRepository;
|
||||
|
||||
/**
|
||||
* 创建普通订单
|
||||
@@ -321,6 +325,17 @@ public class OrderService {
|
||||
throw new RuntimeException("无效的状态转换");
|
||||
}
|
||||
|
||||
// 拼团订单发货校验:必须成团后才能发货
|
||||
if (newStatus == 3 && order.getOrderType() != null && order.getOrderType() == 3) {
|
||||
if (order.getGroupBuyingGroupId() == null) {
|
||||
throw new RuntimeException("拼团订单数据异常,缺少团组信息");
|
||||
}
|
||||
GroupBuyingGroup group = groupBuyingGroupRepository.findById(order.getGroupBuyingGroupId()).orElse(null);
|
||||
if (group == null || group.getStatus() != 2) {
|
||||
throw new RuntimeException("拼团尚未成团,不能发货");
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
order.setStatus(newStatus);
|
||||
if (remark != null && !remark.trim().isEmpty()) {
|
||||
@@ -371,6 +386,20 @@ public class OrderService {
|
||||
throw new RuntimeException("订单状态不正确,无法支付");
|
||||
}
|
||||
|
||||
// 拼团订单校验:团组必须存在且未过期/未失败
|
||||
if (order.getOrderType() != null && order.getOrderType() == 3 && order.getGroupBuyingGroupId() != null) {
|
||||
GroupBuyingGroup group = groupBuyingGroupRepository.findById(order.getGroupBuyingGroupId()).orElse(null);
|
||||
if (group == null) {
|
||||
throw new RuntimeException("拼团团组不存在");
|
||||
}
|
||||
if (group.getStatus() == 3) {
|
||||
throw new RuntimeException("拼团已失败,无法支付");
|
||||
}
|
||||
if (group.getStatus() == 1 && LocalDateTime.now().isAfter(group.getExpireTime())) {
|
||||
throw new RuntimeException("拼团已过期,无法支付");
|
||||
}
|
||||
}
|
||||
|
||||
order.setStatus(2);
|
||||
order.setPaymentMethod(paymentMethod);
|
||||
order.setPaidAt(LocalDateTime.now());
|
||||
@@ -532,10 +561,13 @@ public class OrderService {
|
||||
statistics.setShippedOrders(orderRepository.countByStatus(3));
|
||||
statistics.setCompletedOrders(orderRepository.countByStatus(4));
|
||||
statistics.setCancelledOrders(orderRepository.countByStatus(5));
|
||||
statistics.setRefundingOrders(orderRepository.countByStatus(6));
|
||||
statistics.setRefundedOrders(orderRepository.countByStatus(7));
|
||||
|
||||
// 订单类型统计
|
||||
statistics.setNormalOrders(orderRepository.countByOrderType(1));
|
||||
statistics.setFlashSaleOrders(orderRepository.countByOrderType(2));
|
||||
statistics.setGroupBuyingOrders(orderRepository.countByOrderType(3));
|
||||
|
||||
// 金额统计(这里简化处理,实际应该从数据库聚合查询)
|
||||
List<Order> allOrders = orderRepository.findAll();
|
||||
|
||||
@@ -95,9 +95,9 @@ public class ProductReviewService {
|
||||
return toDTO(review);
|
||||
}
|
||||
|
||||
public ProductReviewDTO.CheckDTO checkReviewStatus(Long orderId, Long productId) {
|
||||
public ProductReviewDTO.CheckDTO checkReviewStatus(Long userId, Long orderId, Long productId) {
|
||||
ProductReviewDTO.CheckDTO checkDTO = new ProductReviewDTO.CheckDTO();
|
||||
Optional<ProductReview> review = productReviewRepository.findByOrderIdAndProductId(orderId, productId);
|
||||
Optional<ProductReview> review = productReviewRepository.findByOrderIdAndUserIdAndProductId(orderId, userId, productId);
|
||||
checkDTO.setReviewed(review.isPresent());
|
||||
review.ifPresent(r -> checkDTO.setReview(toDTO(r)));
|
||||
return checkDTO;
|
||||
|
||||
132
src/main/resources/sql/fix-groupbuying-data.sql
Normal file
132
src/main/resources/sql/fix-groupbuying-data.sql
Normal file
@@ -0,0 +1,132 @@
|
||||
-- ============================================================
|
||||
-- 拼团历史数据修复脚本
|
||||
-- 修复内容:
|
||||
-- 1. 已满员但状态仍为 FORMING(1) 的团组 → 更新为 SUCCESS(2)
|
||||
-- 2. 已成团团组的成员状态从 已加入(1) → 已成团(2)
|
||||
-- 3. 拼团订单的 order_type 修正为 3(拼团订单)
|
||||
-- 4. 已过期且未满员的 FORMING 团组 → 更新为 FAILED(3)
|
||||
-- 5. 已失败团组的成员状态更新
|
||||
-- 使用方式:mysql -u root -p flash_sale_db < fix-groupbuying-data.sql
|
||||
-- ============================================================
|
||||
|
||||
-- 先查看当前数据状况(仅查询,不修改)
|
||||
SELECT '====== 修复前数据概况 ======' AS info;
|
||||
|
||||
SELECT '满员但仍为FORMING的团组' AS category, COUNT(*) AS cnt
|
||||
FROM group_buying_group
|
||||
WHERE status = 1 AND current_members >= required_members;
|
||||
|
||||
SELECT '已过期但仍为FORMING的团组' AS category, COUNT(*) AS cnt
|
||||
FROM group_buying_group
|
||||
WHERE status = 1 AND expire_time < NOW();
|
||||
|
||||
SELECT '拼团订单但order_type不是3的订单' AS category, COUNT(*) AS cnt
|
||||
FROM orders
|
||||
WHERE group_buying_group_id IS NOT NULL AND order_type != 3;
|
||||
|
||||
SELECT '缺少order_items记录的拼团订单' AS category, COUNT(*) AS cnt
|
||||
FROM orders o
|
||||
WHERE o.group_buying_group_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = o.id);
|
||||
|
||||
-- ============================================================
|
||||
-- 修复1: 已满员的团组 → SUCCESS(2)
|
||||
-- ============================================================
|
||||
UPDATE group_buying_group
|
||||
SET status = 2,
|
||||
completed_at = COALESCE(completed_at, NOW())
|
||||
WHERE status = 1
|
||||
AND current_members >= required_members;
|
||||
|
||||
SELECT CONCAT('修复1完成: ', ROW_COUNT(), ' 个满员团组已更新为已成团') AS result;
|
||||
|
||||
-- ============================================================
|
||||
-- 修复2: 已成团(status=2)团组的成员状态 → 已成团(2)
|
||||
-- ============================================================
|
||||
UPDATE group_buying_member m
|
||||
INNER JOIN group_buying_group g ON m.group_id = g.id
|
||||
SET m.status = 2
|
||||
WHERE g.status = 2
|
||||
AND m.status = 1;
|
||||
|
||||
SELECT CONCAT('修复2完成: ', ROW_COUNT(), ' 个成员状态已更新为已成团') AS result;
|
||||
|
||||
-- ============================================================
|
||||
-- 修复3: 已过期且未满员的团组 → FAILED(3)
|
||||
-- ============================================================
|
||||
UPDATE group_buying_group
|
||||
SET status = 3
|
||||
WHERE status = 1
|
||||
AND expire_time < NOW()
|
||||
AND current_members < required_members;
|
||||
|
||||
SELECT CONCAT('修复3完成: ', ROW_COUNT(), ' 个过期团组已更新为已失败') AS result;
|
||||
|
||||
-- ============================================================
|
||||
-- 修复4: 已失败(status=3)团组的成员状态 → 已退出(3)
|
||||
-- ============================================================
|
||||
UPDATE group_buying_member m
|
||||
INNER JOIN group_buying_group g ON m.group_id = g.id
|
||||
SET m.status = 3
|
||||
WHERE g.status = 3
|
||||
AND m.status = 1;
|
||||
|
||||
SELECT CONCAT('修复4完成: ', ROW_COUNT(), ' 个失败团组成员已更新为已退出') AS result;
|
||||
|
||||
-- ============================================================
|
||||
-- 修复5: 拼团关联订单的 order_type 修正为 3
|
||||
-- ============================================================
|
||||
UPDATE orders
|
||||
SET order_type = 3
|
||||
WHERE group_buying_group_id IS NOT NULL
|
||||
AND order_type != 3;
|
||||
|
||||
SELECT CONCAT('修复5完成: ', ROW_COUNT(), ' 个拼团订单的order_type已修正') AS result;
|
||||
|
||||
-- ============================================================
|
||||
-- 修复6: 补充缺失的 order_items 记录(拼团订单)
|
||||
-- ============================================================
|
||||
INSERT INTO order_items (order_id, product_id, product_name, product_image_url, price, quantity, subtotal, created_at)
|
||||
SELECT o.id,
|
||||
o.product_id,
|
||||
COALESCE(p.name, '未知商品'),
|
||||
p.image_url,
|
||||
o.total_price / o.quantity,
|
||||
o.quantity,
|
||||
o.total_price,
|
||||
o.created_at
|
||||
FROM orders o
|
||||
LEFT JOIN products p ON o.product_id = p.id
|
||||
WHERE o.group_buying_group_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = o.id);
|
||||
|
||||
SELECT CONCAT('修复6完成: ', ROW_COUNT(), ' 条缺失的订单明细已补充') AS result;
|
||||
|
||||
-- ============================================================
|
||||
-- 修复7: 已失败团组关联的待支付订单 → 取消(5)
|
||||
-- ============================================================
|
||||
UPDATE orders o
|
||||
INNER JOIN group_buying_group g ON o.group_buying_group_id = g.id
|
||||
SET o.status = 5
|
||||
WHERE g.status = 3
|
||||
AND o.status = 1;
|
||||
|
||||
SELECT CONCAT('修复7完成: ', ROW_COUNT(), ' 个失败团组的待支付订单已取消') AS result;
|
||||
|
||||
-- 修复后数据验证
|
||||
SELECT '====== 修复后数据验证 ======' AS info;
|
||||
|
||||
SELECT status,
|
||||
CASE status WHEN 1 THEN '拼团中' WHEN 2 THEN '已成团' WHEN 3 THEN '已失败' END AS status_desc,
|
||||
COUNT(*) AS cnt
|
||||
FROM group_buying_group
|
||||
GROUP BY status
|
||||
ORDER BY status;
|
||||
|
||||
SELECT '仍有异常的团组(满员但非SUCCESS)' AS category, COUNT(*) AS cnt
|
||||
FROM group_buying_group
|
||||
WHERE current_members >= required_members AND status != 2;
|
||||
|
||||
SELECT '仍有异常的团组(过期但仍FORMING)' AS category, COUNT(*) AS cnt
|
||||
FROM group_buying_group
|
||||
WHERE expire_time < NOW() AND status = 1;
|
||||
Reference in New Issue
Block a user