会员批量导入功能

main
lijun 2026-02-14 23:43:35 +08:00
parent e17411b470
commit 39c8a49d43
7 changed files with 414 additions and 2 deletions

View File

@ -53,7 +53,9 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
}
default List<CouponTemplateDO> selectListByTakeType(Integer takeType) {
return selectList(CouponTemplateDO::getTakeType, takeType, CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
return selectList(new LambdaQueryWrapperX<CouponTemplateDO>()
.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
.eq(CouponTemplateDO::getTakeType, takeType));
}
default List<CouponTemplateDO> selectList(List<Integer> canTakeTypes, Integer productScope, Long productScopeValue, Integer count) {

View File

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import com.yanzhu.framework.common.pojo.CommonResult;
import com.yanzhu.framework.common.pojo.PageResult;
import com.yanzhu.module.member.controller.admin.user.vo.*;
import org.springframework.web.multipart.MultipartFile;
import com.yanzhu.module.member.convert.user.MemberUserConvert;
import com.yanzhu.module.member.dal.dataobject.group.MemberGroupDO;
import com.yanzhu.module.member.dal.dataobject.level.MemberLevelDO;
@ -28,6 +29,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import java.util.stream.Collectors;
import static com.yanzhu.framework.common.pojo.CommonResult.success;
@ -128,4 +131,19 @@ public class MemberUserController {
return success(MemberUserConvert.INSTANCE.convertPage(pageResult, tags, levels, groups));
}
@PostMapping("/import")
@Operation(summary = "导入会员用户")
@PreAuthorize("@ss.hasPermission('member:user:import')")
public CommonResult<MemberUserImportRespVO> importUser(MultipartFile file) {
MemberUserImportRespVO respVO = memberUserService.importUser(file);
return success(respVO);
}
@GetMapping("/import-template")
@Operation(summary = "下载会员导入模板")
@PreAuthorize("@ss.hasPermission('member:user:import')")
public void downloadImportTemplate(HttpServletResponse response) throws IOException {
memberUserService.downloadImportTemplate(response);
}
}

View File

@ -0,0 +1,28 @@
package com.yanzhu.module.member.controller.admin.user.vo;
import lombok.Data;
/**
* VO
*
* @author
*/
@Data
public class MemberUserImportRespVO {
/**
*
*/
private int successCount;
/**
*
*/
private int failCount;
/**
*
*/
private String failReason;
}

View File

@ -5,12 +5,15 @@ import com.yanzhu.framework.common.pojo.PageResult;
import com.yanzhu.framework.common.validation.Mobile;
import com.yanzhu.module.member.controller.admin.user.vo.MemberUserBaseVO;
import com.yanzhu.module.member.controller.admin.user.vo.MemberUserCreateRespVO;
import com.yanzhu.module.member.controller.admin.user.vo.MemberUserImportRespVO;
import com.yanzhu.module.member.controller.admin.user.vo.MemberUserPageReqVO;
import com.yanzhu.module.member.controller.admin.user.vo.MemberUserUpdateReqVO;
import com.yanzhu.module.member.controller.app.user.vo.*;
import com.yanzhu.module.member.dal.dataobject.user.MemberUserDO;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
@ -197,4 +200,20 @@ public interface MemberUserService {
*/
boolean updateUserPoint(Long userId, Integer point);
/**
*
*
* @param file Excel
* @return
*/
MemberUserImportRespVO importUser(MultipartFile file);
/**
*
*
* @param response Http
* @throws IOException IO
*/
void downloadImportTemplate(javax.servlet.http.HttpServletResponse response) throws IOException;
}

View File

@ -12,14 +12,19 @@ import com.yanzhu.framework.common.util.object.BeanUtils;
import com.yanzhu.framework.common.util.servlet.ServletUtils;
import com.yanzhu.module.member.controller.admin.user.vo.MemberUserBaseVO;
import com.yanzhu.module.member.controller.admin.user.vo.MemberUserCreateRespVO;
import com.yanzhu.module.member.controller.admin.user.vo.MemberUserImportRespVO;
import com.yanzhu.module.member.controller.admin.user.vo.MemberUserPageReqVO;
import com.yanzhu.module.member.controller.admin.user.vo.MemberUserUpdateReqVO;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.multipart.MultipartFile;
import com.yanzhu.module.member.controller.app.user.vo.*;
import com.yanzhu.module.member.convert.auth.AuthConvert;
import com.yanzhu.module.member.convert.user.MemberUserConvert;
import com.yanzhu.module.member.dal.dataobject.user.MemberUserDO;
import com.yanzhu.module.member.dal.mysql.user.MemberUserMapper;
import com.yanzhu.module.member.mq.producer.user.MemberUserProducer;
import com.yanzhu.module.member.service.user.MemberUserService;
import com.yanzhu.module.system.api.sms.SmsCodeApi;
import com.yanzhu.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.yanzhu.module.system.api.social.SocialClientApi;
@ -34,7 +39,9 @@ import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
@ -370,4 +377,205 @@ public class MemberUserServiceImpl implements MemberUserService {
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public MemberUserImportRespVO importUser(MultipartFile file) {
MemberUserImportRespVO respVO = new MemberUserImportRespVO();
int successCount = 0;
int failCount = 0;
StringBuilder failReason = new StringBuilder();
try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) {
Sheet sheet = workbook.getSheetAt(0);
int rowCount = sheet.getPhysicalNumberOfRows();
// 跳过表头,从第二行开始读取数据
for (int i = 1; i < rowCount; i++) {
Row row = sheet.getRow(i);
if (row == null) {
continue;
}
try {
// 读取手机号
Cell mobileCell = row.getCell(0);
if (mobileCell == null) {
failCount++;
failReason.append("第").append(i + 1).append("行:手机号为空\n");
continue;
}
String mobile = mobileCell.getStringCellValue().trim();
if (mobile.isEmpty()) {
failCount++;
failReason.append("第").append(i + 1).append("行:手机号为空\n");
continue;
}
// 读取昵称
String nickname = "";
Cell nicknameCell = row.getCell(1);
if (nicknameCell != null) {
nickname = nicknameCell.getStringCellValue().trim();
}
// 读取姓名
String name = "";
Cell nameCell = row.getCell(2);
if (nameCell != null) {
name = nameCell.getStringCellValue().trim();
}
// 读取性别
Integer sex = null;
Cell sexCell = row.getCell(3);
if (sexCell != null) {
String sexStr = sexCell.getStringCellValue().trim();
if ("男".equals(sexStr)) {
sex = 1;
} else if ("女".equals(sexStr)) {
sex = 0;
}
}
// 读取状态
Byte status = 1; // 默认启用
Cell statusCell = row.getCell(4);
if (statusCell != null) {
String statusStr = statusCell.getStringCellValue().trim();
if ("禁用".equals(statusStr)) {
status = 0;
}
}
// 读取等级ID
Long levelId = null;
Cell levelIdCell = row.getCell(5);
if (levelIdCell != null) {
try {
levelId = (long) levelIdCell.getNumericCellValue();
} catch (Exception e) {
// 忽略等级ID解析错误
}
}
// 读取分组ID
Long groupId = null;
Cell groupIdCell = row.getCell(6);
if (groupIdCell != null) {
try {
groupId = (long) groupIdCell.getNumericCellValue();
} catch (Exception e) {
// 忽略分组ID解析错误
}
}
// 创建会员用户
MemberUserBaseVO createReqVO = new MemberUserBaseVO();
createReqVO.setMobile(mobile);
createReqVO.setNickname(nickname);
createReqVO.setName(name);
createReqVO.setSex(sex);
createReqVO.setStatus(status);
createReqVO.setLevelId(levelId);
createReqVO.setGroupId(groupId);
// 校验手机唯一
try {
validateMobileUnique(null, mobile);
// 创建用户
MemberUserDO user = new MemberUserDO();
user.setMobile(mobile);
if (status != null) {
user.setStatus(Integer.valueOf(status));
}
user.setNickname(nickname);
user.setName(name);
user.setSex(sex);
// 使用手机号码的后6位作为密码
String password = mobile.substring(mobile.length() - 6);
user.setPassword(encodePassword(password));
// 设置注册信息
user.setRegisterIp(ServletUtils.getClientIP());
user.setRegisterTerminal(TerminalEnum.H5.getTerminal());
// 插入数据库
memberUserMapper.insert(user);
// 发送 MQ 消息:用户创建
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
memberUserProducer.sendUserCreateMessage(user.getId());
}
});
successCount++;
} catch (Exception e) {
failCount++;
failReason.append("第").append(i + 1).append("行:").append(e.getMessage()).append("\n");
}
} catch (Exception e) {
failCount++;
failReason.append("第").append(i + 1).append("行:处理失败,").append(e.getMessage()).append("\n");
}
}
} catch (Exception e) {
respVO.setFailCount(1);
respVO.setFailReason("文件解析失败:" + e.getMessage());
return respVO;
}
respVO.setSuccessCount(successCount);
respVO.setFailCount(failCount);
respVO.setFailReason(failReason.toString());
return respVO;
}
@Override
public void downloadImportTemplate(HttpServletResponse response) throws IOException {
// 创建工作簿
try (Workbook workbook = new XSSFWorkbook()) {
// 创建工作表
Sheet sheet = workbook.createSheet("会员导入模板");
// 创建表头行
Row headerRow = sheet.createRow(0);
// 设置表头单元格
headerRow.createCell(0).setCellValue("手机号");
headerRow.createCell(1).setCellValue("昵称");
headerRow.createCell(2).setCellValue("姓名");
headerRow.createCell(3).setCellValue("性别");
headerRow.createCell(4).setCellValue("状态");
headerRow.createCell(5).setCellValue("等级ID");
headerRow.createCell(6).setCellValue("分组ID");
// 创建示例数据行
Row exampleRow = sheet.createRow(1);
exampleRow.createCell(0).setCellValue("13800138000");
exampleRow.createCell(1).setCellValue("示例用户1");
exampleRow.createCell(2).setCellValue("张三");
exampleRow.createCell(3).setCellValue("男");
exampleRow.createCell(4).setCellValue("启用");
exampleRow.createCell(5).setCellValue(1);
exampleRow.createCell(6).setCellValue(1);
// 自动调整列宽
for (int i = 0; i < 7; i++) {
sheet.autoSizeColumn(i);
}
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=member_import_template.xlsx");
// 写入响应
workbook.write(response.getOutputStream());
response.flushBuffer();
}
}
}

View File

@ -51,3 +51,13 @@ export const updateUserLevel = async (data: any) => {
export const updateUserPoint = async (data: any) => {
return await request.put({ url: `/member/user/update-point`, data })
}
// 导入会员用户
export const importUser = async (data: FormData) => {
return await request.post({ url: `/member/user/import`, data, headers: { 'Content-Type': 'multipart/form-data' } })
}
// 下载会员导入模板
export const importUserTemplate = () => {
return request.download({ url: `/member/user/import-template` })
}

View File

@ -58,10 +58,14 @@
<MemberGroupSelect v-model="queryParams.groupId" />
</el-form-item>
<el-form-item>
<el-button v-hasPermi="['member:user:create']" @click="openForm('create')">
<el-button @click="openForm('create')">
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button @click="handleImport">
<Icon class="mr-5px" icon="ep:upload-filled" />
导入
</el-button>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
@ -200,11 +204,62 @@
<UserBalanceUpdateForm ref="UpdateBalanceFormRef" @success="getList" />
<!-- 发送优惠券弹窗 -->
<CouponSendForm ref="couponSendFormRef" />
<!-- 导入弹窗 -->
<el-dialog
v-model="importDialogVisible"
title="导入会员"
width="500px"
destroy-on-close
>
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="importUrl"
:auto-upload="false"
:disabled="importLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitImportError"
:on-exceed="handleExceed"
:on-success="submitImportSuccess"
accept=".xlsx, .xls"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport" />
是否更新已经存在的用户数据
</div>
<span>仅允许导入 xlsxlsx 格式文件</span>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="downloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="flex justify-end">
<el-button :disabled="importLoading" @click="importDialogVisible = false">取消</el-button>
<el-button type="primary" :disabled="importLoading" @click="submitImport" :loading="importLoading">
开始导入
</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime'
import * as UserApi from '@/api/member/user'
import { DICT_TYPE } from '@/utils/dict'
import download from '@/utils/download'
import UserForm from './UserForm.vue'
import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
@ -279,6 +334,78 @@ const openCoupon = () => {
}
couponSendFormRef.value.open(selectedIds.value)
}
//
import { getAccessToken, getTenantId } from '@/utils/auth'
const importDialogVisible = ref(false)
const uploadRef = ref()
const importLoading = ref(false)
const fileList = ref([])
const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/member/user/import'
const uploadHeaders = ref()
const updateSupport = ref(0)
//
const handleImport = () => {
importDialogVisible.value = true
fileList.value = []
updateSupport.value = 0
}
//
const submitImport = async () => {
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
//
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
importLoading.value = true
uploadRef.value!.submit()
}
//
const submitImportSuccess = (response: any) => {
importLoading.value = false
if (response.code !== 0) {
message.error(response.msg)
return
}
//
const data = response.data
let text = '成功导入数量:' + data.successCount + ';'
text += '失败导入数量:' + data.failCount + ';'
if (data.failCount > 0) {
text += '失败原因:' + data.failReason
}
message.alert(text)
getList() //
importDialogVisible.value = false
}
//
const submitImportError = (): void => {
importLoading.value = false
message.error('上传失败,请您重新上传!')
}
//
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
//
const downloadTemplate = async () => {
try {
const res = await UserApi.importUserTemplate()
download.excel(res, '会员导入模板.xlsx')
} catch (error) {
message.error('下载模板失败:' + error.message)
}
}
/** 操作分发 */
const handleCommand = (command: string, row: UserApi.UserVO) => {
switch (command) {