CRM:code review 客户导入

This commit is contained in:
YunaiV 2024-02-23 18:50:22 +08:00
parent a64da48aa3
commit e53a0ca884
33 changed files with 277 additions and 217 deletions

View File

@ -257,11 +257,11 @@ public class CollectionUtils {
return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
}
public static <T> T findFirst(List<T> from, Predicate<T> predicate) {
public static <T> T findFirst(Collection<T> from, Predicate<T> predicate) {
return findFirst(from, predicate, Function.identity());
}
public static <T, U> U findFirst(List<T> from, Predicate<T> predicate, Function<T, U> func) {
public static <T, U> U findFirst(Collection<T> from, Predicate<T> predicate, Function<T, U> func) {
if (CollUtil.isEmpty(from)) {
return null;
}

View File

@ -4,7 +4,6 @@ import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.csv.CsvRow;
import cn.hutool.core.text.csv.CsvUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
@ -72,26 +71,25 @@ public class AreaUtils {
* @param id 区域编号
* @return 区域
*/
public static Area getArea(Integer id) {
public static Area parseArea(Integer id) {
return areas.get(id);
}
/**
* 获得指定区域对应的编号
*
* @param path 区域编号
* @return 编号
* @param pathStr 区域路径例如说河南省/石家庄市/新华区
* @return 区域
*/
public static Area getArea(String path) {
String[] paths = path.split("/");
public static Area parseArea(String pathStr) {
String[] paths = pathStr.split("/");
Area area = null;
for (int i = 0; i < paths.length; i++) {
final int finalI = i;
for (String path : paths) {
if (area == null) {
area = findFirst(convertList(areas.values(), a -> a), item -> ObjUtil.equal(paths[finalI], item.getName()));
continue;
area = findFirst(areas.values(), item -> item.getName().equals(path));
} else {
area = findFirst(area.getChildren(), item -> item.getName().equals(path));
}
area = findFirst(area.getChildren(), item -> ObjUtil.equal(paths[finalI], item.getName()));
}
return area;
}
@ -102,9 +100,9 @@ public class AreaUtils {
* @param areas 地区树
* @return 所有节点的全路径名称
*/
public static List<String> getAllAreaNodePaths(List<Area> areas) {
public static List<String> getAreaNodePathList(List<Area> areas) {
List<String> paths = new ArrayList<>();
areas.forEach(area -> traverse(area, "", paths));
areas.forEach(area -> getAreaNodePathList(area, "", paths));
return paths;
}
@ -113,9 +111,9 @@ public class AreaUtils {
*
* @param node 父节点
* @param path 全路径名称
* @param paths 全路径名称列表
* @param paths 全路径名称列表省份/城市/地区
*/
private static void traverse(Area node, String path, List<String> paths) {
private static void getAreaNodePathList(Area node, String path, List<String> paths) {
if (node == null) {
return;
}
@ -124,7 +122,7 @@ public class AreaUtils {
paths.add(currentPath);
// 递归遍历子节点
for (Area child : node.getChildren()) {
traverse(child, currentPath, paths);
getAreaNodePathList(child, currentPath, paths);
}
}
@ -195,7 +193,7 @@ public class AreaUtils {
*/
public static Integer getParentIdByType(Integer id, @NonNull AreaTypeEnum type) {
for (int i = 0; i < Byte.MAX_VALUE; i++) {
Area area = AreaUtils.getArea(id);
Area area = AreaUtils.parseArea(id);
if (area == null) {
return null;
}

View File

@ -72,7 +72,7 @@ public class IPUtils {
* @return 地区
*/
public static Area getArea(String ip) {
return AreaUtils.getArea(getAreaId(ip));
return AreaUtils.parseArea(getAreaId(ip));
}
/**
@ -82,6 +82,6 @@ public class IPUtils {
* @return 地区
*/
public static Area getArea(long ip) {
return AreaUtils.getArea(getAreaId(ip));
return AreaUtils.parseArea(getAreaId(ip));
}
}

View File

@ -17,7 +17,7 @@ public class AreaUtilsTest {
@Test
public void testGetArea() {
// 调用北京
Area area = AreaUtils.getArea(110100);
Area area = AreaUtils.parseArea(110100);
// 断言
assertEquals(area.getId(), 110100);
assertEquals(area.getName(), "北京市");

View File

@ -49,7 +49,7 @@
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 ExcelUtils 使用 -->
<optional>true</optional> <!-- 设置为 optional只有在 AreaConvert 的时候使用 -->
</dependency>
</dependencies>

View File

@ -33,7 +33,7 @@ public class AreaConvert implements Converter<Object> {
GlobalConfiguration globalConfiguration) {
// 解析地区编号
String label = readCellData.getStringValue();
Area area = AreaUtils.getArea(label);
Area area = AreaUtils.parseArea(label);
if (area == null) {
log.error("[convertToJavaData][label({}) 解析不掉]", label);
return null;

View File

@ -19,28 +19,35 @@ import java.util.List;
*/
public class SelectSheetWriteHandler implements SheetWriteHandler {
private static final String DICT_SHEET_NAME = "字典sheet";
// TODO @puhui999key 不使用 int 值么感觉不是很优雅哈
private final List<KeyValue<Integer, List<String>>> selectMap;
private static final char[] ALPHABET = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
public SelectSheetWriteHandler(List<KeyValue<Integer, List<String>>> selectMap) {
if (CollUtil.isEmpty(selectMap)) {
this.selectMap = null;
return;
}
selectMap.sort(Comparator.comparing(item -> item.getValue().size())); // 升序不然创建下拉会报错
this.selectMap = selectMap;
}
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
if (selectMap == null || CollUtil.isEmpty(selectMap)) {
if (CollUtil.isEmpty(selectMap)) {
return;
}
// 需要设置下拉框的 sheet
Sheet curSheet = writeSheetHolder.getSheet();
DataValidationHelper helper = curSheet.getDataValidationHelper();
String dictSheetName = "字典sheet";
Sheet currentSheet = writeSheetHolder.getSheet();
DataValidationHelper helper = currentSheet.getDataValidationHelper();
Workbook workbook = writeWorkbookHolder.getWorkbook();
// 数据字典的 sheet
Sheet dictSheet = workbook.createSheet(dictSheetName);
Sheet dictSheet = workbook.createSheet(DICT_SHEET_NAME);
for (KeyValue<Integer, List<String>> keyValue : selectMap) {
// 设置下拉单元格的首行末行首列末列
CellRangeAddressList rangeAddressList = new CellRangeAddressList(1, 65533, keyValue.getKey(), keyValue.getKey());
@ -53,18 +60,18 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
}
row.createCell(keyValue.getKey()).setCellValue(keyValue.getValue().get(i));
}
String excelColumn = getExcelColumn(keyValue.getKey());
// 下拉框数据来源 eg:字典sheet!$B1:$B2
String refers = dictSheetName + "!$" + excelColumn + "$1:$" + excelColumn + "$" + rowLen;
// 创建可被其他单元格引用的名称
// TODO @puhui999下面 1. 2.1 2.2 2.3 我是按照已经理解的调整了下格式这样可读性更好 52 62 你可以看看是不是也弄下序号
// 1. 创建可被其他单元格引用的名称
Name name = workbook.createName();
// 设置名称的名字
name.setNameName("dict" + keyValue.getKey());
// 设置公式
name.setRefersToFormula(refers);
// 设置引用约束
DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey());
// 设置约束
// TODO @puhui999下面的 excelColumn refers 两行是不是可以封装成一个方法替代 getExcelColumn
String excelColumn = getExcelColumn(keyValue.getKey());
String refers = DICT_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + rowLen; // 下拉框数据来源 eg:字典sheet!$B1:$B2
name.setNameName("dict" + keyValue.getKey()); // 设置名称的名字
name.setRefersToFormula(refers); // 设置公式
// 2.1 设置约束
DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey()); // 设置引用约束
DataValidation validation = helper.createValidation(constraint, rangeAddressList);
if (validation instanceof HSSFDataValidation) {
validation.setSuppressDropDownArrow(false);
@ -72,10 +79,10 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
validation.setSuppressDropDownArrow(true);
validation.setShowErrorBox(true);
}
// 阻止输入非下拉框的值
// 2.2 阻止输入非下拉框的值
validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
validation.createErrorBox("提示", "此值不存在于下拉选择中!");
// 添加下拉框约束
// 2.3 添加下拉框约束
writeSheetHolder.getSheet().addValidationData(validation);
}
}
@ -86,8 +93,9 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
* @param num 数字
* @return 字母
*/
// TODO @puhui999这个是必须字母列哇还是数字其实也可以哈主要想看看怎么能把这个逻辑进一步简化
private String getExcelColumn(int num) {
String column = "";
String column;
int len = ALPHABET.length - 1;
int first = num / len;
int second = num % len;
@ -96,9 +104,9 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
} else {
column = ALPHABET[first - 1] + "";
if (second == 0) {
column = column + ALPHABET[len] + "";
column = column + ALPHABET[len];
} else {
column = column + ALPHABET[second - 1] + "";
column = column + ALPHABET[second - 1];
}
}
return column;

View File

@ -33,15 +33,7 @@ public class ExcelUtils {
*/
public static <T> void write(HttpServletResponse response, String filename, String sheetName,
Class<T> head, List<T> data) throws IOException {
// 输出 Excel
EasyExcel.write(response.getOutputStream(), head)
.autoCloseStream(false) // 不要自动关闭交给 Servlet 自己处理
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度自动适配最大 255 宽度
.registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
.sheet(sheetName).doWrite(data);
// 设置 header contentType写在最后的原因是避免报错时响应 contentType 已经被修改了
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
write(response, filename, sheetName, head, data, null);
}
/**

View File

@ -14,6 +14,7 @@ public interface ErrorCodeConstants {
ErrorCode CONTRACT_UPDATE_FAIL_NOT_DRAFT = new ErrorCode(1_020_000_001, "合同更新失败,原因:合同不是草稿状态");
ErrorCode CONTRACT_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_020_000_002, "合同提交审核失败,原因:合同没处在未提交状态");
ErrorCode CONTRACT_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_020_000_003, "更新合同审核状态失败,原因:合同不是审核中状态");
ErrorCode CONTRACT_NO_EXISTS = new ErrorCode(1_020_000_004, "生成合同序列号重复,请重试");
// ========== 线索管理 1-020-001-000 ==========
ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");

View File

@ -13,26 +13,18 @@ import java.util.Arrays;
*/
@Getter
@AllArgsConstructor
public enum CrmReturnTypeEnum implements IntArrayValuable {
public enum CrmReceivableReturnTypeEnum implements IntArrayValuable {
// 支票
CHECK(1, "支票"),
// 现金
CASH(2, "现金"),
// 邮政汇款
POSTAL_REMITTANCE(3, "邮政汇款"),
// 电汇
TELEGRAPHIC_TRANSFER(4, "电汇"),
// 网上转账
ONLINE_TRANSFER(5, "网上转账"),
// 支付宝
ALIPAY(6, "支付宝"),
// 微信支付
WECHAT_PAY(7, "微信支付"),
// 其他
OTHER(8, "其它");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmReturnTypeEnum::getType).toArray();
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmReceivableReturnTypeEnum::getType).toArray();
/**
* 类型

View File

@ -268,6 +268,24 @@ public class CrmCustomerController {
ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list, builderSelectMap());
}
private List<KeyValue<Integer, List<String>>> builderSelectMap() {
List<KeyValue<Integer, List<String>>> selectMap = new ArrayList<>();
// 获取地区下拉数据
// TODO @puhui999嘿嘿这里改成省份城市区域三个选项难度大么
Area area = AreaUtils.parseArea(Area.ID_CHINA);
selectMap.add(new KeyValue<>(6, AreaUtils.getAreaNodePathList(area.getChildren())));
// 获取客户所属行业
List<String> customerIndustries = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_INDUSTRY);
selectMap.add(new KeyValue<>(8, customerIndustries));
// 获取客户等级
List<String> customerLevels = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_LEVEL);
selectMap.add(new KeyValue<>(9, customerLevels));
// 获取客户来源
List<String> customerSources = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_SOURCE);
selectMap.add(new KeyValue<>(10, customerSources));
return selectMap;
}
@PostMapping("/import")
@Operation(summary = "导入客户")
@PreAuthorize("@ss.hasPermission('system:customer:import')")
@ -321,21 +339,4 @@ public class CrmCustomerController {
return success(true);
}
private List<KeyValue<Integer, List<String>>> builderSelectMap() {
List<KeyValue<Integer, List<String>>> selectMap = new ArrayList<>();
// 获取地区下拉数据
Area area = AreaUtils.getArea(Area.ID_CHINA);
selectMap.add(new KeyValue<>(6, AreaUtils.getAllAreaNodePaths(area.getChildren())));
// 获取客户所属行业
List<String> customerIndustries = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_INDUSTRY);
selectMap.add(new KeyValue<>(8, customerIndustries));
// 获取客户等级
List<String> customerLevels = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_LEVEL);
selectMap.add(new KeyValue<>(9, customerLevels));
// 获取客户来源
List<String> customerSources = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_SOURCE);
selectMap.add(new KeyValue<>(10, customerSources));
return selectMap;
}
}

View File

@ -107,7 +107,6 @@ public class CrmReceivableController {
return success(buildReceivableDetailPage(pageResult));
}
// TODO 芋艿后面在优化导出
@GetMapping("/export-excel")
@Operation(summary = "导出回款 Excel")
@PreAuthorize("@ss.hasPermission('crm:receivable:export')")

View File

@ -28,7 +28,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -56,7 +55,6 @@ public class CrmReceivablePlanController {
@Resource
private CrmReceivableService receivableService;
@Resource
@Lazy
private CrmContractService contractService;
@Resource
private CrmCustomerService customerService;

View File

@ -6,6 +6,7 @@ import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
// TODO @puhui999缺导出
@Schema(description = "管理后台 - CRM 回款计划 Response VO")
@Data
public class CrmReceivablePlanRespVO {
@ -13,17 +14,38 @@ public class CrmReceivablePlanRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long id;
@Schema(description = "回款编号", example = "19852")
private Long receivableId;
@Schema(description = "期数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer period;
@Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long customerId;
@Schema(description = "客户名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "test")
private String customerName;
@Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long contractId;
@Schema(description = "合同编号", example = "Q110")
private String contractNo;
@Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long ownerUserId;
@Schema(description = "负责人", example = "test")
private String ownerUserName;
@Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
private LocalDateTime returnTime;
@Schema(description = "回款方式", example = "1")
private Integer returnType; // 来自 Receivable returnType 字段
@Schema(description = "计划回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
private BigDecimal price;
@Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
private LocalDateTime returnTime;
@Schema(description = "回款编号", example = "19852")
private Long receivableId;
@Schema(description = "完成状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean finishStatus;
@Schema(description = "提前几天提醒", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer remindDays;
@ -31,43 +53,16 @@ public class CrmReceivablePlanRespVO {
@Schema(description = "提醒日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
private LocalDateTime remindTime;
@Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long customerId;
@Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long contractId;
@Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long ownerUserId;
@Schema(description = "显示顺序")
private Integer sort;
@Schema(description = "备注", example = "备注")
private String remark;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "客户名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "test")
private String customerName;
@Schema(description = "合同编号", example = "Q110")
private String contractNo;
@Schema(description = "负责人", example = "test")
private String ownerUserName;
@Schema(description = "创建人", example = "25682")
private String creator;
@Schema(description = "创建人名字", example = "test")
private String creatorName;
@Schema(description = "完成状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Boolean finishStatus;
@Schema(description = "回款方式", example = "1")
private Integer returnType; // 来自 Receivable returnType 字段
}

View File

@ -14,26 +14,6 @@ public class CrmReceivablePlanSaveReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long id;
@Schema(description = "期数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotNull(message = "期数不能为空")
private Integer period;
@Schema(description = "计划回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
@NotNull(message = "计划回款金额不能为空")
private BigDecimal price;
@Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
@NotNull(message = "计划回款日期不能为空")
private LocalDateTime returnTime;
@Schema(description = "提前几天提醒", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "提前几天提醒不能为空")
private Integer remindDays;
@Schema(description = "提醒日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
@NotNull(message = "提醒日期不能为空")
private LocalDateTime remindTime;
@Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotNull(message = "客户编号不能为空")
private Long customerId;
@ -46,8 +26,19 @@ public class CrmReceivablePlanSaveReqVO {
@NotNull(message = "负责人编号不能为空")
private Long ownerUserId;
@Schema(description = "显示顺序")
private Integer sort;
@Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
@NotNull(message = "计划回款日期不能为空")
private LocalDateTime returnTime;
@Schema(description = "回款方式", example = "1")
private Integer returnType;
@Schema(description = "计划回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
@NotNull(message = "计划回款金额不能为空")
private BigDecimal price;
@Schema(description = "提前几天提醒", example = "1")
private Integer remindDays;
@Schema(description = "备注", example = "备注")
private String remark;

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.crm.enums.receivable.CrmReturnTypeEnum;
import cn.iocoder.yudao.module.crm.enums.receivable.CrmReceivableReturnTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@ -23,7 +23,7 @@ public class CrmReceivableSaveReqVO {
private Long planId; // 不是通过回款计划创建的回款没有回款计划编号
@Schema(description = "回款方式", example = "2")
@InEnum(CrmReturnTypeEnum.class)
@InEnum(CrmReceivableReturnTypeEnum.class)
private Integer returnType;
@Schema(description = "回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")

View File

@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import cn.iocoder.yudao.module.crm.enums.receivable.CrmReturnTypeEnum;
import cn.iocoder.yudao.module.crm.enums.receivable.CrmReceivableReturnTypeEnum;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
@ -60,7 +60,7 @@ public class CrmReceivableDO extends BaseDO {
*/
private LocalDateTime returnTime;
/**
* 回款方式,关联枚举{@link CrmReturnTypeEnum}
* 回款方式,关联枚举{@link CrmReceivableReturnTypeEnum}
*/
private Integer returnType;
/**

View File

@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.crm.dal.dataobject.receivable;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import cn.iocoder.yudao.module.crm.enums.receivable.CrmReceivableReturnTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@ -13,7 +13,7 @@ import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 回款计划 DO
* CRM 回款计划 DO
*
* @author 芋道源码
*/
@ -37,17 +37,49 @@ public class CrmReceivablePlanDO extends BaseDO {
*/
private Integer period;
/**
* 回款编号关联 {@link CrmReceivableDO#getId()}
* 客户编号
*
* 关联 {@link CrmCustomerDO#getId()}
*/
private Long receivableId;
private Long customerId;
/**
* 计划回款金额单位
* 合同编号
*
* 关联 {@link CrmContractDO#getId()}
*/
private BigDecimal price;
private Long contractId;
/**
* 负责人编号
*
* 关联 AdminUserDO id 字段
*/
private Long ownerUserId;
/**
* 计划回款日期
*/
private LocalDateTime returnTime;
/**
* 回款类型
*
* 枚举 {@link CrmReceivableReturnTypeEnum}
*/
private Integer returnType;
/**
* 计划回款金额单位
*/
private BigDecimal price;
/**
* 回款编号关联 {@link CrmReceivableDO#getId()}
*/
private Long receivableId;
/**
* 完成状态
*/
private Boolean finishStatus;
/**
* 提前几天提醒
*/
@ -56,30 +88,9 @@ public class CrmReceivablePlanDO extends BaseDO {
* 提醒日期
*/
private LocalDateTime remindTime;
/**
* 客户编号关联 {@link CrmCustomerDO#getId()}
*/
private Long customerId;
/**
* 合同编号关联 {@link CrmContractDO#getId()}
*/
private Long contractId;
/**
* 负责人编号关联 {@link AdminUserRespDTO#getId()}
*/
private Long ownerUserId;
/**
* 显示顺序
*/
private Integer sort;
/**
* 备注
*/
private String remark;
/**
* 完成状态
*/
private Boolean finishStatus;
}

View File

@ -11,7 +11,6 @@ import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
import cn.iocoder.yudao.module.crm.util.CrmQueryWrapperUtils;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
@ -26,10 +25,8 @@ import java.util.List;
@Mapper
public interface CrmContractMapper extends BaseMapperX<CrmContractDO> {
default int updateOwnerUserIdById(Long id, Long ownerUserId) {
return update(new LambdaUpdateWrapper<CrmContractDO>()
.eq(CrmContractDO::getId, id)
.set(CrmContractDO::getOwnerUserId, ownerUserId));
default CrmContractDO selectByNo(String no) {
return selectOne(CrmContractDO::getNo, no);
}
default PageResult<CrmContractDO> selectPageByCustomerId(CrmContractPageReqVO pageReqVO) {

View File

@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.crm.dal.redis;
/**
* CRM Redis Key 枚举类
*
* @author 芋道源码
*/
public interface RedisKeyConstants {
/**
* 序号的缓存
*
* KEY 格式trade_no:{prefix}
* VALUE 数据格式编号自增
*/
String NO = "crm:seq_no:";
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.crm.dal.redis.no;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.module.crm.dal.redis.RedisKeyConstants;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* Crm 订单序号的 Redis DAO
*
* @author HUIHUI
*/
@Repository
public class CrmNoRedisDAO {
/**
* 合同 {@link cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO}
*/
public static final String CONTRACT_NO_PREFIX = "HT";
/**
* 还款 {@link cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO}
*/
public static final String RECEIVABLE_PREFIX = "HK";
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 生成序号使用当前日期格式为 {PREFIX} + yyyyMMdd + 6 位自增
* 例如说QTRK 202109 000001 没有中间空格
*
* @param prefix 前缀
* @return 序号
*/
public String generate(String prefix) {
// 递增序号
String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
String key = RedisKeyConstants.NO + noPrefix;
Long no = stringRedisTemplate.opsForValue().increment(key);
// 设置过期时间
stringRedisTemplate.expire(key, Duration.ofDays(1L));
return noPrefix + String.format("%06d", no);
}
}

View File

@ -17,6 +17,7 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractProductDO;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractProductMapper;
import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
@ -69,6 +70,9 @@ public class CrmContractServiceImpl implements CrmContractService {
@Resource
private CrmContractProductMapper contractProductMapper;
@Resource
private CrmNoRedisDAO noRedisDAO;
@Resource
private CrmPermissionService crmPermissionService;
@Resource
@ -94,11 +98,14 @@ public class CrmContractServiceImpl implements CrmContractService {
List<CrmContractProductDO> contractProducts = validateContractProducts(createReqVO.getProducts());
// 1.2 校验关联字段
validateRelationDataExists(createReqVO);
// TODO 芋艿生成 no
// 1.3 生成序号
String no = noRedisDAO.generate(CrmNoRedisDAO.CONTRACT_NO_PREFIX);
if (contractMapper.selectByNo(no) != null) {
throw exception(CONTRACT_NO_EXISTS);
}
// 2.1 插入合同
CrmContractDO contract = BeanUtils.toBean(createReqVO, CrmContractDO.class);
contract.setNo(System.currentTimeMillis() + ""); // TODO
CrmContractDO contract = BeanUtils.toBean(createReqVO, CrmContractDO.class).setNo(no);
calculateTotalPrice(contract, contractProducts);
contractMapper.insert(contract);
// 2.2 插入合同关联商品
@ -247,7 +254,7 @@ public class CrmContractServiceImpl implements CrmContractService {
crmPermissionService.transferPermission(new CrmPermissionTransferReqBO(userId, CrmBizTypeEnum.CRM_CONTRACT.getType(),
reqVO.getId(), reqVO.getNewOwnerUserId(), reqVO.getOldOwnerPermissionLevel()));
// 2.2 设置负责人
contractMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
contractMapper.updateById(new CrmContractDO().setId(reqVO.getId()).setOwnerUserId(reqVO.getNewOwnerUserId()));
// 3. 记录转移日志
LogRecordContext.putVariable("contract", contract);

View File

@ -23,7 +23,7 @@ import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import org.hibernate.validator.internal.util.stereotypes.Lazy;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@ -57,6 +57,7 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
private CrmCustomerService customerService;
@Resource
private CrmPermissionService permissionService;
@Resource
private AdminUserApi adminUserApi;
@ -72,15 +73,16 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
int period = (int) (count + 1);
createReqVO.setPeriod(createReqVO.getPeriod() != period ? period : createReqVO.getPeriod()); // 如果期数不对则纠正
// 2.1 插入
// 2. 插入还款计划
CrmReceivablePlanDO receivablePlan = BeanUtils.toBean(createReqVO, CrmReceivablePlanDO.class).setId(null).setFinishStatus(false);
receivablePlanMapper.insert(receivablePlan);
// 2.2 创建数据权限
// 3. 创建数据权限
permissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(createReqVO.getOwnerUserId())
.setBizType(CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType()).setBizId(receivablePlan.getId())
.setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
// 3. 记录操作日志上下文
// 4. 记录操作日志上下文
LogRecordContext.putVariable("receivablePlan", receivablePlan);
return receivablePlan.getId();
}

View File

@ -31,6 +31,7 @@ import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@ -66,10 +67,12 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
private CrmContractService contractService;
@Resource
private CrmCustomerService customerService;
@Resource
// @Resource
@Lazy // 延迟加载避免循环依赖
private CrmReceivablePlanService receivablePlanService;
@Resource
private CrmPermissionService permissionService;
@Resource
private AdminUserApi adminUserApi;
@Resource

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.crm.util;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
@ -18,23 +17,11 @@ public class CrmAuditStatusUtils {
* @param bpmResult BPM 审批结果
*/
public static Integer convertBpmResultToAuditStatus(Integer bpmResult) {
Assert.isTrue(isEndResult(bpmResult), "BPM 审批结果({}) 转换失败, 流程状态不是最终结果", bpmResult);
Integer auditStatus = BpmProcessInstanceResultEnum.APPROVE.getResult().equals(bpmResult) ? CrmAuditStatusEnum.APPROVE.getStatus()
: BpmProcessInstanceResultEnum.REJECT.getResult().equals(bpmResult) ? CrmAuditStatusEnum.REJECT.getStatus()
: BpmProcessInstanceResultEnum.CANCEL.getResult();
: BpmProcessInstanceResultEnum.CANCEL.getResult().equals(bpmResult) ? BpmProcessInstanceResultEnum.CANCEL.getResult() : null;
Assert.notNull(auditStatus, "BPM 审批结果({}) 转换失败", bpmResult);
return auditStatus;
}
/**
* 判断该结果是否处于 End 最终结果
*
* @param bpmResult BPM 审批结果
* @return 是否
*/
public static boolean isEndResult(Integer bpmResult) {
return ObjectUtils.equalsAny(bpmResult, BpmProcessInstanceResultEnum.APPROVE.getResult(),
BpmProcessInstanceResultEnum.REJECT.getResult(), BpmProcessInstanceResultEnum.CANCEL.getResult());
}
}

View File

@ -13,6 +13,6 @@ public interface RedisKeyConstants {
* KEY 格式trade_no:{prefix}
* VALUE 数据格式编号自增
*/
String NO = "seq_no:";
String NO = "erp:seq_no:";
}

View File

@ -12,7 +12,7 @@ import java.time.LocalDateTime;
/**
* 订单序号的 Redis DAO
* Erp 订单序号的 Redis DAO
*
* @author HUIHUI
*/

View File

@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* 字典数据 API 接口
*
@ -40,12 +42,23 @@ public interface DictDataApi {
*/
DictDataRespDTO parseDictData(String type, String label);
/**
* 获得指定字典类型的字典数据列表
*
* @param dictType 字典类型
* @return 字典数据列表
*/
List<DictDataRespDTO> getDictDataList(String dictType);
/**
* 获得字典数据标签列表
*
* @param dictType 字典类型
* @return 字典数据标签列表
*/
List<String> getDictDataLabelList(String dictType);
default List<String> getDictDataLabelList(String dictType) {
List<DictDataRespDTO> list = getDictDataList(dictType);
return convertList(list, DictDataRespDTO::getLabel);
}
}

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.system.api.dict;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
@ -9,11 +8,8 @@ import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* 字典数据 API 实现类
*
@ -43,12 +39,9 @@ public class DictDataApiImpl implements DictDataApi {
}
@Override
public List<String> getDictDataLabelList(String dictType) {
List<DictDataDO> dictDataList = dictDataService.getDictDataListByDictType(dictType);
if (CollUtil.isEmpty(dictDataList)) {
return Collections.emptyList();
}
return convertList(dictDataList, DictDataDO::getLabel);
public List<DictDataRespDTO> getDictDataList(String dictType) {
List<DictDataDO> list = dictDataService.getDictDataListByDictType(dictType);
return BeanUtils.toBean(list, DictDataRespDTO.class);
}
}

View File

@ -29,7 +29,7 @@ public class AreaController {
@GetMapping("/tree")
@Operation(summary = "获得地区树")
public CommonResult<List<AreaNodeRespVO>> getAreaTree() {
Area area = AreaUtils.getArea(Area.ID_CHINA);
Area area = AreaUtils.parseArea(Area.ID_CHINA);
Assert.notNull(area, "获取不到中国");
return success(BeanUtils.toBean(area.getChildren(), AreaNodeRespVO.class));
}

View File

@ -26,7 +26,7 @@ public class AppAreaController {
@GetMapping("/tree")
@Operation(summary = "获得地区树")
public CommonResult<List<AppAreaNodeRespVO>> getAreaTree() {
Area area = AreaUtils.getArea(Area.ID_CHINA);
Area area = AreaUtils.parseArea(Area.ID_CHINA);
Assert.notNull(area, "获取不到中国");
return success(BeanUtils.toBean(area.getChildren(), AppAreaNodeRespVO.class));
}

View File

@ -100,7 +100,7 @@ public interface DictDataService {
DictDataDO parseDictData(String dictType, String label);
/**
* 获得字典数据列表
* 获得指定数据类型的字典数据列表
*
* @param dictType 字典类型
* @return 字典数据列表

View File

@ -171,7 +171,9 @@ public class DictDataServiceImpl implements DictDataService {
@Override
public List<DictDataDO> getDictDataListByDictType(String dictType) {
return dictDataMapper.selectList(DictDataDO::getDictType, dictType);
List<DictDataDO> list = dictDataMapper.selectList(DictDataDO::getDictType, dictType);
list.sort(Comparator.comparing(DictDataDO::getSort));
return list;
}
}