从第三方获取用户优惠券
This commit is contained in:
526
src/main/java/com/rj/service/impl/LbUserCouponServiceImpl.java
Normal file
526
src/main/java/com/rj/service/impl/LbUserCouponServiceImpl.java
Normal file
@@ -0,0 +1,526 @@
|
||||
package com.rj.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.rj.dto.hxr.HxrMoneyCouponApiContext;
|
||||
import com.rj.dto.hxr.HxrUserLoginApiContext;
|
||||
import com.rj.entity.LbThirdIntegrationConfig;
|
||||
import com.rj.entity.LbUser;
|
||||
import com.rj.entity.LbUserCoupon;
|
||||
import com.rj.mapper.LbUserCouponMapper;
|
||||
import com.rj.service.HxrAdminUserLoginService;
|
||||
import com.rj.service.ILbThirdIntegrationConfigService;
|
||||
import com.rj.service.ILbUserCouponService;
|
||||
import com.rj.service.ILbUserService;
|
||||
import com.rj.util.HxrGoodsSignUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class LbUserCouponServiceImpl
|
||||
extends ServiceImpl<LbUserCouponMapper, LbUserCoupon>
|
||||
implements ILbUserCouponService {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private static final String DEFAULT_SIMULATE_LOGIN_PASSWORD = "123456";
|
||||
private static final String USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
+ "(KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0";
|
||||
private static final String HEADER_TOKEN = "token";
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
private static final ObjectMapper JSON = new ObjectMapper()
|
||||
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
private final ILbUserService lbUserService;
|
||||
private final ILbThirdIntegrationConfigService lbThirdIntegrationConfigService;
|
||||
private final HxrAdminUserLoginService hxrAdminUserLoginService;
|
||||
|
||||
public LbUserCouponServiceImpl(
|
||||
ILbUserService lbUserService,
|
||||
ILbThirdIntegrationConfigService lbThirdIntegrationConfigService,
|
||||
HxrAdminUserLoginService hxrAdminUserLoginService) {
|
||||
this.lbUserService = lbUserService;
|
||||
this.lbThirdIntegrationConfigService = lbThirdIntegrationConfigService;
|
||||
this.hxrAdminUserLoginService = hxrAdminUserLoginService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> syncUserCoupons(String tenantId, Integer recentDays) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
if (tenantId == null || tenantId.trim().isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "tenantId不能为空");
|
||||
return result;
|
||||
}
|
||||
if (recentDays == null || recentDays < 1) {
|
||||
result.put("success", false);
|
||||
result.put("message", "recentDays必须大于0");
|
||||
return result;
|
||||
}
|
||||
|
||||
String tid = tenantId.trim();
|
||||
|
||||
// 获取第三方集成配置
|
||||
LbThirdIntegrationConfig config = lbThirdIntegrationConfigService.getOne(
|
||||
new LambdaQueryWrapper<LbThirdIntegrationConfig>()
|
||||
.eq(LbThirdIntegrationConfig::getTenantId, tid)
|
||||
.eq(LbThirdIntegrationConfig::getEnabled, 1)
|
||||
.last("LIMIT 1"));
|
||||
if (config == null) {
|
||||
result.put("success", false);
|
||||
result.put("message", "未找到该租户的第三方集成配置,或配置未启用");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 获取登录上下文
|
||||
Optional<HxrUserLoginApiContext> loginCtxOpt =
|
||||
lbThirdIntegrationConfigService.resolveUserLoginApiContext(tid);
|
||||
if (loginCtxOpt.isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "未找到登录API配置");
|
||||
return result;
|
||||
}
|
||||
HxrUserLoginApiContext loginCtx = loginCtxOpt.get();
|
||||
|
||||
// 计算日期范围:现在 - recentDays 天
|
||||
LocalDateTime startDate = LocalDateTime.now().minusDays(recentDays);
|
||||
|
||||
// 查询用户列表,根据 updated_at 过滤,mobile 和 nickname 去重
|
||||
List<LbUser> allUsers = lbUserService.list(
|
||||
new LambdaQueryWrapper<LbUser>()
|
||||
.eq(LbUser::getTenantId, tid)
|
||||
.ge(LbUser::getUpdatedAt, startDate)
|
||||
.isNotNull(LbUser::getMobile)
|
||||
.ne(LbUser::getMobile, "")
|
||||
.orderByDesc(LbUser::getUpdatedAt)
|
||||
.last("LIMIT 5000"));
|
||||
|
||||
// 根据 mobile 和 nickname 去重
|
||||
Map<String, LbUser> uniqueUserMap = new LinkedHashMap<>();
|
||||
for (LbUser user : allUsers) {
|
||||
String key = user.getMobile() + "|" + (user.getNickname() != null ? user.getNickname() : "");
|
||||
uniqueUserMap.put(key, user);
|
||||
}
|
||||
List<LbUser> userList = new ArrayList<>(uniqueUserMap.values());
|
||||
|
||||
log.info("同步用户优惠券:tenantId={}, recentDays={}, 过滤后用户数={}", tid, recentDays, userList.size());
|
||||
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (LbUser user : userList) {
|
||||
String mobile = user.getMobile();
|
||||
if (!StringUtils.hasText(mobile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用登录接口
|
||||
HxrAdminUserLoginService.LoginApiResult loginResult =
|
||||
hxrAdminUserLoginService.login(mobile, DEFAULT_SIMULATE_LOGIN_PASSWORD, loginCtx);
|
||||
if (!loginResult.success()) {
|
||||
log.warn("用户登录失败 mobile={} apiCode={} apiMsg={}", mobile, loginResult.apiCode(), loginResult.apiMsg());
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 提取 token
|
||||
String token = HxrAdminUserLoginService.extractToken(loginResult.parsed());
|
||||
if (!StringUtils.hasText(token)) {
|
||||
log.warn("登录成功但无法提取token mobile={}", mobile);
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取优惠券API上下文
|
||||
Optional<HxrMoneyCouponApiContext> couponCtxOpt =
|
||||
lbThirdIntegrationConfigService.resolveMoneyCouponApiContext(tid, token);
|
||||
if (couponCtxOpt.isEmpty()) {
|
||||
log.warn("无法获取优惠券API上下文 mobile={}", mobile);
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
HxrMoneyCouponApiContext couponCtx = couponCtxOpt.get();
|
||||
|
||||
// 调用优惠券列表API
|
||||
String couponJson = fetchMoneyCouponList(couponCtx, recentDays);
|
||||
if (couponJson == null || couponJson.isBlank()) {
|
||||
log.warn("优惠券列表为空 mobile={}", mobile);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析并保存优惠券
|
||||
int saved = parseAndSaveCoupons(couponJson, user, tid);
|
||||
successCount += saved;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户优惠券异常 mobile={}", mobile, e);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
result.put("success", true);
|
||||
result.put("message", "同步完成");
|
||||
result.put("totalUsers", userList.size());
|
||||
result.put("successCount", successCount);
|
||||
result.put("failCount", failCount);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("同步用户优惠券异常", e);
|
||||
result.put("success", false);
|
||||
result.put("message", "同步异常:" + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private String fetchMoneyCouponList(HxrMoneyCouponApiContext ctx, int recentDays) throws Exception {
|
||||
String couponApiBaseUrl = ctx.couponApiBaseUrl();
|
||||
if (!StringUtils.hasText(couponApiBaseUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int pageLimit = recentDays > 0 ? recentDays : 90;
|
||||
String uri = couponApiBaseUrl + (couponApiBaseUrl.contains("?") ? "&" : "?") + "limit=" + pageLimit;
|
||||
|
||||
long timestamp = System.currentTimeMillis() / 1000;
|
||||
String noncestr = randomNoncestr();
|
||||
Map<String, Object> signParams = new LinkedHashMap<>();
|
||||
signParams.put("timestamp", timestamp);
|
||||
signParams.put("noncestr", noncestr);
|
||||
String sign = HxrGoodsSignUtil.computeSign(signParams, ctx.appStr().trim());
|
||||
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(60))
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.timeout(Duration.ofSeconds(120))
|
||||
.header("Accept", "application/json,*/*")
|
||||
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
.header("Origin", ctx.origin())
|
||||
.header("Referer", ctx.referer())
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header(HEADER_TOKEN, ctx.token())
|
||||
.header("S", sign)
|
||||
.header("T", String.valueOf(timestamp))
|
||||
.header("N", noncestr)
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
int status = response.statusCode();
|
||||
if (status < 200 || status >= 300) {
|
||||
log.warn("优惠券API HTTP {} bodyPrefix={}", status, abbreviate(response.body(), 400));
|
||||
return null;
|
||||
}
|
||||
return response.body();
|
||||
}
|
||||
|
||||
private int parseAndSaveCoupons(String couponJson, LbUser user, String tenantId) throws Exception {
|
||||
JsonNode root = JSON.readTree(couponJson);
|
||||
int code = root.path("code").asInt(-1);
|
||||
if (code != 0) {
|
||||
log.warn("优惠券API返回错误 code={} msg={}", code, root.path("msg").asText(""));
|
||||
return 0;
|
||||
}
|
||||
|
||||
JsonNode listNode = root.path("data").path("list");
|
||||
if (listNode.isMissingNode() || !listNode.isArray()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int savedCount = 0;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
for (JsonNode item : listNode) {
|
||||
try {
|
||||
LbUserCoupon coupon = new LbUserCoupon();
|
||||
coupon.setId(UUID.randomUUID().toString());
|
||||
coupon.setTenantId(tenantId);
|
||||
coupon.setUserId(String.valueOf(user.getId()));
|
||||
coupon.setUserPhone(user.getMobile());
|
||||
coupon.setUserNickname(user.getNickname());
|
||||
coupon.setParentPhone(user.getPmobile());
|
||||
coupon.setParentNickname(user.getPname());
|
||||
|
||||
// 从JSON中解析金额和日期
|
||||
String moneyStr = item.path("money").asText("0");
|
||||
coupon.setCouponAmount(new BigDecimal(moneyStr));
|
||||
|
||||
String createdAtStr = item.path("created_at").asText(null);
|
||||
if (createdAtStr != null && !createdAtStr.isBlank()) {
|
||||
try {
|
||||
LocalDate couponDate = LocalDate.parse(createdAtStr.substring(0, 10), DATE_FORMATTER);
|
||||
coupon.setCouponDate(couponDate);
|
||||
} catch (Exception e) {
|
||||
log.warn("日期解析失败 created_at={}", createdAtStr);
|
||||
}
|
||||
}
|
||||
|
||||
coupon.setCreateTime(now);
|
||||
coupon.setUpdateTime(now);
|
||||
|
||||
this.save(coupon);
|
||||
savedCount++;
|
||||
} catch (Exception e) {
|
||||
log.warn("保存优惠券记录异常 item={}", item, e);
|
||||
}
|
||||
}
|
||||
|
||||
return savedCount;
|
||||
}
|
||||
|
||||
private static String randomNoncestr() {
|
||||
String base36 = Long.toUnsignedString(Math.abs(RANDOM.nextLong()), 36);
|
||||
if (base36.length() >= 5) {
|
||||
return base36.substring(base36.length() - 5);
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(base36);
|
||||
while (sb.length() < 5) {
|
||||
sb.append(Integer.toString(RANDOM.nextInt(36), 36));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String abbreviate(String s, int maxLen) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
return s.length() <= maxLen ? s : s.substring(0, maxLen) + "...";
|
||||
}
|
||||
|
||||
private LocalDate parseDate(String dateStr) {
|
||||
if (dateStr == null || dateStr.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(dateStr.trim(), DATE_FORMATTER);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> add(LbUserCoupon entity) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
if (entity.getTenantId() == null || entity.getTenantId().trim().isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "tenantId不能为空");
|
||||
return result;
|
||||
}
|
||||
entity.setTenantId(entity.getTenantId().trim());
|
||||
if (entity.getUserPhone() != null) {
|
||||
entity.setUserPhone(entity.getUserPhone().trim());
|
||||
}
|
||||
if (entity.getUserNickname() != null) {
|
||||
entity.setUserNickname(entity.getUserNickname().trim());
|
||||
}
|
||||
if (entity.getParentPhone() != null) {
|
||||
entity.setParentPhone(entity.getParentPhone().trim());
|
||||
}
|
||||
if (entity.getParentNickname() != null) {
|
||||
entity.setParentNickname(entity.getParentNickname().trim());
|
||||
}
|
||||
if (entity.getCouponAmount() == null) {
|
||||
entity.setCouponAmount(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
if (entity.getId() == null || entity.getId().trim().isEmpty()) {
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (entity.getCreateTime() == null) {
|
||||
entity.setCreateTime(now);
|
||||
}
|
||||
entity.setUpdateTime(now);
|
||||
|
||||
boolean ok = this.save(entity);
|
||||
result.put("success", ok);
|
||||
result.put("message", ok ? "新增成功" : "新增失败");
|
||||
if (ok) {
|
||||
result.put("data", entity);
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("用户优惠券新增异常", e);
|
||||
result.put("success", false);
|
||||
result.put("message", "新增异常:" + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> update(LbUserCoupon entity) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
if (entity.getId() == null || entity.getId().trim().isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "id不能为空");
|
||||
return result;
|
||||
}
|
||||
if (entity.getTenantId() != null) {
|
||||
entity.setTenantId(entity.getTenantId().trim());
|
||||
}
|
||||
if (entity.getUserPhone() != null) {
|
||||
entity.setUserPhone(entity.getUserPhone().trim());
|
||||
}
|
||||
if (entity.getUserNickname() != null) {
|
||||
entity.setUserNickname(entity.getUserNickname().trim());
|
||||
}
|
||||
if (entity.getParentPhone() != null) {
|
||||
entity.setParentPhone(entity.getParentPhone().trim());
|
||||
}
|
||||
if (entity.getParentNickname() != null) {
|
||||
entity.setParentNickname(entity.getParentNickname().trim());
|
||||
}
|
||||
entity.setUpdateTime(LocalDateTime.now());
|
||||
boolean ok = this.updateById(entity);
|
||||
result.put("success", ok);
|
||||
result.put("message", ok ? "编辑成功" : "编辑失败");
|
||||
if (ok) {
|
||||
result.put("data", this.getById(entity.getId()));
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("用户优惠券编辑异常", e);
|
||||
result.put("success", false);
|
||||
result.put("message", "编辑异常:" + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> deleteById(String id) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
if (id == null || id.trim().isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "id不能为空");
|
||||
return result;
|
||||
}
|
||||
boolean ok = this.removeById(id.trim());
|
||||
result.put("success", ok);
|
||||
result.put("message", ok ? "删除成功" : "删除失败");
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("用户优惠券删除异常", e);
|
||||
result.put("success", false);
|
||||
result.put("message", "删除异常:" + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> pageQuery(Integer current,
|
||||
Integer size,
|
||||
String tenantId,
|
||||
String userId,
|
||||
String userPhone,
|
||||
String userNickname,
|
||||
String parentPhone,
|
||||
String parentNickname,
|
||||
String couponDateStart,
|
||||
String couponDateEnd) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
if (current == null || current < 1) {
|
||||
current = 1;
|
||||
}
|
||||
if (size == null || size < 1) {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<LbUserCoupon> queryWrapper = new LambdaQueryWrapper<>();
|
||||
if (tenantId != null && !tenantId.trim().isEmpty()) {
|
||||
queryWrapper.eq(LbUserCoupon::getTenantId, tenantId.trim());
|
||||
}
|
||||
if (userId != null && !userId.trim().isEmpty()) {
|
||||
queryWrapper.eq(LbUserCoupon::getUserId, userId.trim());
|
||||
}
|
||||
if (userPhone != null && !userPhone.trim().isEmpty()) {
|
||||
queryWrapper.like(LbUserCoupon::getUserPhone, userPhone.trim());
|
||||
}
|
||||
if (userNickname != null && !userNickname.trim().isEmpty()) {
|
||||
queryWrapper.like(LbUserCoupon::getUserNickname, userNickname.trim());
|
||||
}
|
||||
if (parentPhone != null && !parentPhone.trim().isEmpty()) {
|
||||
queryWrapper.like(LbUserCoupon::getParentPhone, parentPhone.trim());
|
||||
}
|
||||
if (parentNickname != null && !parentNickname.trim().isEmpty()) {
|
||||
queryWrapper.like(LbUserCoupon::getParentNickname, parentNickname.trim());
|
||||
}
|
||||
|
||||
LocalDate start = parseDate(couponDateStart);
|
||||
if (couponDateStart != null && !couponDateStart.trim().isEmpty() && start == null) {
|
||||
result.put("success", false);
|
||||
result.put("message", "couponDateStart 格式错误,请使用 yyyy-MM-dd");
|
||||
return result;
|
||||
}
|
||||
LocalDate end = parseDate(couponDateEnd);
|
||||
if (couponDateEnd != null && !couponDateEnd.trim().isEmpty() && end == null) {
|
||||
result.put("success", false);
|
||||
result.put("message", "couponDateEnd 格式错误,请使用 yyyy-MM-dd");
|
||||
return result;
|
||||
}
|
||||
if (start != null) {
|
||||
queryWrapper.ge(LbUserCoupon::getCouponDate, start);
|
||||
}
|
||||
if (end != null) {
|
||||
queryWrapper.le(LbUserCoupon::getCouponDate, end);
|
||||
}
|
||||
|
||||
queryWrapper.orderByDesc(LbUserCoupon::getUpdateTime)
|
||||
.orderByDesc(LbUserCoupon::getCreateTime);
|
||||
|
||||
Page<LbUserCoupon> page = this.page(new Page<>(current, size), queryWrapper);
|
||||
result.put("success", true);
|
||||
result.put("message", "查询成功");
|
||||
result.put("data", page.getRecords());
|
||||
result.put("total", page.getTotal());
|
||||
result.put("current", page.getCurrent());
|
||||
result.put("size", page.getSize());
|
||||
result.put("pages", page.getPages());
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("用户优惠券分页查询异常", e);
|
||||
result.put("success", false);
|
||||
result.put("message", "查询异常:" + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user