package com.xiaopeng.operation.controller; import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.ExcelImportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.ImportParams; import cn.afterturn.easypoi.excel.entity.TemplateExportParams; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.DesensitizedUtil; import cn.hutool.core.util.PhoneUtil; import cn.hutool.core.io.resource.ClassPathResource; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.xiaopeng.api.feignClient.account.SysGroupMenuClient; import com.xiaopeng.api.model.operation.XpInvitationCode; import com.xiaopeng.api.model.operation.XpWhitelist; import com.xiaopeng.api.vo.account.LoginUser; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.exception.ServiceException; import com.xiaopeng.common.core.utils.DateUtils; import com.xiaopeng.common.core.utils.WebUtils; import com.xiaopeng.common.core.web.page.PageInfo; import com.xiaopeng.common.security.service.TokenService; import com.xiaopeng.operation.constant.I18nKeyConstant; import com.xiaopeng.operation.dto.InvitationAddDto; import com.xiaopeng.operation.exportExcle.XpInvitationCodeExportExcel; import com.xiaopeng.operation.importExcel.InvitationCodeImportExcel; import com.xiaopeng.operation.query.InvitationPageListQuery; import com.xiaopeng.operation.service.IXpInvitationCodeService; import com.xiaopeng.operation.service.IXpWhitelistService; import io.swagger.annotations.*; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.Workbook; import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; import java.util.regex.Pattern; /** *

* 邀请码管理 前端控制器 *

* * @author mitao * @since 2026-02-03 */ @RestController @Api(tags = "邀请码管理") @Slf4j @RequestMapping("") public class XpInvitationCodeController { @Resource private IXpInvitationCodeService xpInvitationCodeService; @Resource private IXpWhitelistService xpWhitelistService; @Resource private MessageSource messageSource; @Resource private SysGroupMenuClient groupMenuClient; @Resource private TokenService tokenService; private static final Pattern CHINESE_PATTERN = Pattern.compile("\\p{IsHan}"); private static final long INVITATION_CODE_LIMIT = 500L; @ApiOperation(value = "邀请码管理分页列表", response = XpInvitationCode.class) @PostMapping("/api/v1/invitation/pageList") public R> pageList(@RequestBody InvitationPageListQuery query) { LoginUser loginUser = tokenService.getLoginUser(); PageInfo pageList = xpInvitationCodeService.pageList(query); if(!loginUser.getPhoneDesensitization()){ pageList.getRecords().forEach(s -> s.setBindAccount(DesensitizedUtil.mobilePhone(s.getBindAccount()))); } return R.ok(pageList); } /** * 生成邀请码 */ @PostMapping("/api/v1/invitation/add") @ResponseBody @ApiOperation(value = "生成邀请码") public R add(@RequestBody InvitationAddDto dto) { long existing = xpInvitationCodeService.count(); long incoming = Arrays.stream(Optional.ofNullable(dto.getPhones()).orElse("").split(",")) .map(String::trim) .filter(StringUtils::hasLength) .count(); if (existing >= INVITATION_CODE_LIMIT || existing + incoming > INVITATION_CODE_LIMIT) { String msg = messageSource.getMessage(I18nKeyConstant.INVITATION_LIMIT_EXCEEDED, new Object[]{INVITATION_CODE_LIMIT, existing, incoming}, I18nKeyConstant.INVITATION_LIMIT_EXCEEDED, LocaleContextHolder.getLocale()); return R.fail(msg); } return xpInvitationCodeService.add(dto.getPhones()); } /** * 解绑 */ @PutMapping("/api/v1/invitation/unbind") @ResponseBody @ApiOperation(value = "解绑") @ApiImplicitParams({ @ApiImplicitParam(value = "id", name = "id", dataType = "Long", required = true) }) public R unbind(@RequestParam("id") Long id) { UpdateWrapper xpInvitationCodeUpdateWrapper = new UpdateWrapper<>(); xpInvitationCodeUpdateWrapper.eq("id", id); xpInvitationCodeUpdateWrapper.set("status", 3); xpInvitationCodeUpdateWrapper.set("bind_account", null); xpInvitationCodeService.update(xpInvitationCodeUpdateWrapper); return R.ok(); } @ApiOperation(value = "邀请码导入模板下载") @GetMapping("/api/v1/invitation/import-template") public void importTemplate() { List invitationCodeImportExcels = new ArrayList<>(); InvitationCodeImportExcel invitationCodeImportExcel = new InvitationCodeImportExcel(); invitationCodeImportExcels.add(invitationCodeImportExcel); Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), InvitationCodeImportExcel.class, invitationCodeImportExcels); HttpServletResponse response = WebUtils.response(); ServletOutputStream outputStream = null; //1.获取excel模板 ClassPathResource classPathResource = new ClassPathResource("template/邀请码导入模板.xlsx"); TemplateExportParams params = new TemplateExportParams(classPathResource.getPath()); response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); try { String fileName = URLEncoder.encode("邀请码导入模板.xlsx", "utf-8"); response.setHeader("Content-dispodition", "attachment;filename=" + fileName); outputStream = response.getOutputStream(); workbook.write(outputStream); } catch (IOException e) { e.printStackTrace(); System.out.println("邀请码导入模板下载失败!"); } finally { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } @ApiOperation(value = "邀请码导入") @PostMapping("/base/v1/invitation/importExcel") public R importExcel(@RequestParam("file") MultipartFile file) { ImportParams params = new ImportParams(); params.setHeadRows(1); //表头行数 InputStream inputStream = null; List invitationCodeImportExcels; try { inputStream = file.getInputStream(); invitationCodeImportExcels = ExcelImportUtil.importExcel(inputStream, InvitationCodeImportExcel.class, params); } catch (Exception e) { e.printStackTrace(); log.error("邀请码导入失败:{}", e.getMessage()); throw new ServiceException("邀请码导入失败!"); } finally { try { if (inputStream != null) { inputStream.close(); } } catch (IOException e) { throw new ServiceException(e.getMessage()); } } if (CollectionUtils.isEmpty(invitationCodeImportExcels)) { return R.fail(messageSource.getMessage(I18nKeyConstant.INVITATION_IMPORT_DATA_EMPTY, null, I18nKeyConstant.INVITATION_IMPORT_DATA_EMPTY, LocaleContextHolder.getLocale())); } long existing = xpInvitationCodeService.count(); long incoming = invitationCodeImportExcels.size(); if (existing >= INVITATION_CODE_LIMIT || existing + incoming > INVITATION_CODE_LIMIT) { String msg = messageSource.getMessage(I18nKeyConstant.INVITATION_LIMIT_EXCEEDED, new Object[]{INVITATION_CODE_LIMIT, existing, incoming}, I18nKeyConstant.INVITATION_LIMIT_EXCEEDED, LocaleContextHolder.getLocale()); return R.fail(msg); } // 批量校验:账号必须为手机号;邀请码不能为空且不能包含中文;同时校验文件内重复/数据库已存在 List errors = new ArrayList<>(); Set importCodes = new HashSet<>(); for (int i = 0; i < invitationCodeImportExcels.size(); i++) { InvitationCodeImportExcel row = invitationCodeImportExcels.get(i); int rowNum = i + 2; String invitationCode = trimToNull(row.getInvitationCode()); String bindAccount = trimToNull(row.getBindAccount()); row.setInvitationCode(invitationCode); row.setBindAccount(bindAccount); if (!StringUtils.hasLength(invitationCode)) { errors.add(messageSource.getMessage(I18nKeyConstant.INVITATION_IMPORT_CODE_EMPTY, new Object[]{rowNum}, I18nKeyConstant.INVITATION_IMPORT_CODE_EMPTY, LocaleContextHolder.getLocale())); } else { if (containsChinese(invitationCode)) { errors.add(messageSource.getMessage(I18nKeyConstant.INVITATION_IMPORT_CODE_CONTAINS_CHINESE, new Object[]{rowNum, bindAccount}, I18nKeyConstant.INVITATION_IMPORT_CODE_CONTAINS_CHINESE, LocaleContextHolder.getLocale())); } importCodes.add(invitationCode); } if (!StringUtils.hasLength(bindAccount)) { errors.add(messageSource.getMessage(I18nKeyConstant.INVITATION_IMPORT_ACCOUNT_EMPTY, new Object[]{rowNum}, I18nKeyConstant.INVITATION_IMPORT_ACCOUNT_EMPTY, LocaleContextHolder.getLocale())); } else if (!PhoneUtil.isMobile(bindAccount)) { errors.add(messageSource.getMessage(I18nKeyConstant.INVITATION_IMPORT_ACCOUNT_INVALID, new Object[]{rowNum, bindAccount}, I18nKeyConstant.INVITATION_IMPORT_ACCOUNT_INVALID, LocaleContextHolder.getLocale())); } if (StringUtils.hasLength(row.getBindTime())) { row.setBindTime(row.getBindTime().trim()); } } Map codeCountMap = invitationCodeImportExcels.stream() .map(InvitationCodeImportExcel::getInvitationCode) .filter(StringUtils::hasLength) .collect(Collectors.groupingBy(code -> code, Collectors.counting())); List duplicatedCodes = codeCountMap.entrySet().stream() .filter(entry -> entry.getValue() != null && entry.getValue() > 1) .map(Map.Entry::getKey) .sorted() .collect(Collectors.toList()); if (!duplicatedCodes.isEmpty()) { errors.add(messageSource.getMessage(I18nKeyConstant.INVITATION_IMPORT_CODE_DUPLICATE, new Object[]{String.join(",", duplicatedCodes)}, I18nKeyConstant.INVITATION_IMPORT_CODE_DUPLICATE, LocaleContextHolder.getLocale())); } if (!importCodes.isEmpty()) { List existsList = xpInvitationCodeService.lambdaQuery() .in(XpInvitationCode::getInvitationCode, importCodes) .list(); if (!CollectionUtils.isEmpty(existsList)) { List existsCodes = existsList.stream() .map(XpInvitationCode::getInvitationCode) .filter(StringUtils::hasLength) .distinct() .sorted() .collect(Collectors.toList()); if (!existsCodes.isEmpty()) { errors.add(messageSource.getMessage(I18nKeyConstant.INVITATION_IMPORT_CODE_EXISTS, new Object[]{String.join(",", existsCodes)}, I18nKeyConstant.INVITATION_IMPORT_CODE_EXISTS, LocaleContextHolder.getLocale())); } } } if (!errors.isEmpty()) { return R.fail(messageSource.getMessage(I18nKeyConstant.INVITATION_IMPORT_VALIDATE_FAIL, new Object[]{String.join("\n", errors)}, I18nKeyConstant.INVITATION_IMPORT_VALIDATE_FAIL, LocaleContextHolder.getLocale())); } List addList = new ArrayList<>(); List addWhitelistList = new ArrayList<>(); for (InvitationCodeImportExcel locationExcel : invitationCodeImportExcels) { locationExcel.setBindTime(locationExcel.getBindTime()); XpInvitationCode xpInvitationCode = new XpInvitationCode(); xpInvitationCode.setInvitationCode(locationExcel.getInvitationCode()); xpInvitationCode.setBindAccount(locationExcel.getBindAccount()); if (StringUtils.hasLength(locationExcel.getBindTime())) { String replace = locationExcel.getBindTime().replace(".", "-"); String res = replace.replace("/", "-"); locationExcel.setBindTime(res); } xpInvitationCode.setBindTime(DateUtils.stringToLocalDateTime(locationExcel.getBindTime())); xpInvitationCode.setStatus(2); XpWhitelist xpWhitelist = new XpWhitelist(); xpWhitelist.setAccount(locationExcel.getBindAccount()); xpWhitelist.setStatus(true); xpWhitelist.setIsPriorityDispatch(false); xpWhitelist.setWhitelistSource(2); addWhitelistList.add(xpWhitelist); addList.add(xpInvitationCode); } xpInvitationCodeService.saveBatch(addList); xpWhitelistService.saveBatch(addWhitelistList); return R.ok(); } private static String trimToNull(String value) { if (!StringUtils.hasLength(value)) { return null; } String trimmed = value.trim(); return StringUtils.hasLength(trimmed) ? trimmed : null; } private static boolean containsChinese(String value) { if (!StringUtils.hasLength(value)) { return false; } return CHINESE_PATTERN.matcher(value).find(); } @ApiOperation(value = "邀请码导出") @PostMapping(value = "/api/v1/invitation/exportExcel") public void exportExcel(@RequestBody InvitationPageListQuery query) { List list = xpInvitationCodeService.exportList(query); List xpWhitelistExportExcels = new ArrayList<>(); for (XpInvitationCode xpInvitationCode : list) { XpInvitationCodeExportExcel xpWhitelistExportExcel = new XpInvitationCodeExportExcel(); // xpWhitelistExportExcel.setId(xpInvitationCode.getId()); xpWhitelistExportExcel.setBind_account(xpInvitationCode.getBindAccount()); xpWhitelistExportExcel.setInvitationCode(xpInvitationCode.getInvitationCode()); LocalDateTime createTime = xpInvitationCode.getCreateTime(); LocalDateTime bindTime = xpInvitationCode.getBindTime(); // 将LocalDateTime转化为yyyy-MM-dd HH:mm:ss格式字符串 xpWhitelistExportExcel.setCreateTime(createTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); xpWhitelistExportExcel.setBindTime(bindTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); xpWhitelistExportExcels.add(xpWhitelistExportExcel); } //1.获取excel模板 Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), XpInvitationCodeExportExcel.class, xpWhitelistExportExcels); HttpServletResponse response = WebUtils.response(); response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); ServletOutputStream outputStream = null; try { String fileName = URLEncoder.encode("邀请码列表.xls", "utf-8"); response.setHeader("Content-dispodition", "attachment;filename=" + fileName); outputStream = response.getOutputStream(); workbook.write(outputStream); } catch (IOException e) { e.printStackTrace(); } finally { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 批量删除 */ @GetMapping("/api/v1/invitation/deleteByIds") @ResponseBody @ApiOperation(value = "批量删除") public R deleteByIds(String ids) { // 再转化为Long类型 List longs = Arrays.stream(ids.split(",")).map(Long::parseLong).collect(Collectors.toList()); xpInvitationCodeService.removeBatchByIds(longs); return R.ok(); } @ApiOperation(value = "校验生成邀请码的手机号是否合法") @GetMapping("/api/v1/invitation/validateCode") public R> validateCode(@RequestParam String phone) { List phoneList = Arrays.asList(phone.split(",")); List existPhones = phoneList.stream().filter(item -> xpInvitationCodeService.lambdaQuery() .in(XpInvitationCode::getBindAccount, item) .count() > 0).collect(Collectors.toList()); return R.ok(existPhones); } }