Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
owen 2024-02-04 20:58:38 +08:00
commit 787b0140e7
241 changed files with 5823 additions and 1320 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 204 KiB

View File

@ -235,8 +235,6 @@
![功能图](/.image/common/mall-preview.png)
_前端基于 crmeb uniapp 经过授权重构优化代码实现接入芋道快速开发平台_
演示地址:<https://doc.iocoder.cn/mall-preview/>
### 会员中心

11
pom.xml
View File

@ -15,13 +15,14 @@
<!-- 各种 module 拓展 -->
<module>yudao-module-system</module>
<module>yudao-module-infra</module>
<module>yudao-module-member</module>
<!-- <module>yudao-module-bpm</module>-->
<!-- <module>yudao-module-member</module>-->
<module>yudao-module-bpm</module>
<!-- <module>yudao-module-report</module>-->
<!-- <module>yudao-module-mp</module>-->
<module>yudao-module-pay</module>
<module>yudao-module-mall</module>
<!-- <module>yudao-module-pay</module>-->
<!-- <module>yudao-module-mall</module>-->
<module>yudao-module-crm</module>
<module>yudao-module-erp</module>
<!-- 示例项目 -->
<!-- <module>yudao-example</module>-->
</modules>
@ -41,7 +42,7 @@
<flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version>
<!-- 看看咋放到 bom 里 -->
<lombok.version>1.18.30</lombok.version>
<spring.boot.version>3.2.0</spring.boot.version>
<spring.boot.version>3.2.2</spring.boot.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

View File

@ -74,7 +74,7 @@ function stop() {
if [ -n "$PID" ]; then
echo -e ".\c"
else
echo '[stop] 停止 $BASE_PATH/$SERVER_NAME 成功'
echo "[stop] 停止 $BASE_PATH/$SERVER_NAME 成功"
break
fi
done

File diff suppressed because it is too large Load Diff

View File

@ -17,47 +17,47 @@
<revision>2.0.0-snapshot</revision>
<flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 -->
<spring.boot.version>3.2.0</spring.boot.version>
<spring.boot.version>3.2.2</spring.boot.version>
<!-- Web 相关 -->
<springdoc.version>2.2.0</springdoc.version>
<knife4j.version>4.3.0</knife4j.version>
<!-- DB 相关 -->
<druid.version>1.2.20</druid.version>
<mybatis-plus.version>3.5.4.1</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.4.1</mybatis-plus-generator.version>
<dynamic-datasource.version>4.2.0</dynamic-datasource.version>
<mybatis-plus-join.version>1.4.8.1</mybatis-plus-join.version>
<redisson.version>3.25.0</redisson.version>
<druid.version>1.2.21</druid.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.5</mybatis-plus-generator.version>
<dynamic-datasource.version>4.3.0</dynamic-datasource.version>
<mybatis-plus-join.version>1.4.10</mybatis-plus-join.version>
<redisson.version>3.26.0</redisson.version>
<dm8.jdbc.version>8.1.3.62</dm8.jdbc.version>
<!-- 消息队列 -->
<rocketmq-spring.version>2.2.3</rocketmq-spring.version>
<!-- 服务保障相关 -->
<lock4j.version>2.2.5</lock4j.version>
<lock4j.version>2.2.7</lock4j.version>
<resilience4j.version>2.1.0</resilience4j.version>
<!-- 监控相关 -->
<skywalking.version>9.0.0</skywalking.version>
<spring-boot-admin.version>3.1.8</spring-boot-admin.version>
<spring-boot-admin.version>3.2.1</spring-boot-admin.version>
<opentracing.version>0.33.0</opentracing.version>
<!-- Test 测试相关 -->
<podam.version>8.0.0.RELEASE</podam.version>
<jedis-mock.version>1.0.12</jedis-mock.version>
<podam.version>8.0.1.RELEASE</podam.version>
<jedis-mock.version>1.0.13</jedis-mock.version>
<mockito-inline.version>5.2.0</mockito-inline.version>
<!-- Bpm 工作流相关 -->
<flowable.version>7.0.0</flowable.version>
<flowable.version>7.0.1</flowable.version>
<!-- 工具类相关 -->
<captcha-plus.version>2.0.3</captcha-plus.version>
<jsoup.version>1.17.1</jsoup.version>
<jsoup.version>1.17.2</jsoup.version>
<lombok.version>1.18.30</lombok.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<hutool-5.version>5.8.23</hutool-5.version>
<hutool-6.version>6.0.0-M8</hutool-6.version>
<hutool-5.version>5.8.25</hutool-5.version>
<hutool-6.version>6.0.0-M10</hutool-6.version>
<easyexcel.verion>3.3.3</easyexcel.verion>
<velocity.version>2.3</velocity.version>
<screw.version>1.0.5</screw.version>
<fastjson.version>1.2.83</fastjson.version>
<guava.version>32.1.3-jre</guava.version>
<guava.version>33.0.0-jre</guava.version>
<guice.version>5.1.0</guice.version>
<transmittable-thread-local.version>2.14.4</transmittable-thread-local.version>
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<commons-net.version>3.10.0</commons-net.version>
<jsch.version>0.1.55</jsch.version>
<tika-core.version>2.9.1</tika-core.version>
@ -66,15 +66,15 @@
<!-- 三方云服务相关 -->
<okio.version>3.5.0</okio.version>
<okhttp3.version>4.11.0</okhttp3.version>
<commons-io.version>2.11.0</commons-io.version>
<minio.version>8.5.6</minio.version>
<commons-io.version>2.15.1</commons-io.version>
<minio.version>8.5.7</minio.version>
<aliyun-java-sdk-core.version>4.6.4</aliyun-java-sdk-core.version>
<aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
<tencentcloud-sdk-java.version>3.1.880</tencentcloud-sdk-java.version>
<justauth.version>2.0.5</justauth.version>
<jimureport.version>1.6.6-beta2</jimureport.version>
<xercesImpl.version>2.12.2</xercesImpl.version>
<weixin-java.version>4.5.7.B</weixin-java.version>
<weixin-java.version>4.6.0</weixin-java.version>
<ureport2.version>2.2.9</ureport2.version>
</properties>
@ -90,11 +90,6 @@
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-banner</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
@ -116,11 +111,6 @@
<artifactId>yudao-spring-boot-starter-biz-dict</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-sms</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
@ -136,11 +126,6 @@
<artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-error-code</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-ip</artifactId>

View File

@ -11,7 +11,6 @@
<packaging>pom</packaging>
<modules>
<module>yudao-common</module>
<module>yudao-spring-boot-starter-banner</module>
<module>yudao-spring-boot-starter-mybatis</module>
<module>yudao-spring-boot-starter-redis</module>
<module>yudao-spring-boot-starter-web</module>
@ -28,18 +27,15 @@
<module>yudao-spring-boot-starter-biz-operatelog</module>
<module>yudao-spring-boot-starter-biz-dict</module>
<module>yudao-spring-boot-starter-biz-sms</module>
<module>yudao-spring-boot-starter-biz-pay</module>
<module>yudao-spring-boot-starter-biz-tenant</module>
<module>yudao-spring-boot-starter-biz-data-permission</module>
<module>yudao-spring-boot-starter-biz-error-code</module>
<module>yudao-spring-boot-starter-biz-ip</module>
<module>yudao-spring-boot-starter-flowable</module>
<module>yudao-spring-boot-starter-captcha</module>
<module>yudao-spring-boot-starter-websocket</module>
<module>yudao-spring-boot-starter-desensitize</module>
</modules>
<artifactId>yudao-framework</artifactId>

View File

@ -35,6 +35,22 @@ public class MoneyUtils {
return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue();
}
/**
* 计算百分比金额
*
* @param price 金额单位分
* @param count 数量
* @param percent 折扣单位分列如 60.2%则传入 6020
* @return 商品总价
*/
public static Integer calculator(Integer price, Integer count, Integer percent) {
price = price * count;
if (percent == null) {
return price;
}
return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100));
}
/**
* 计算百分比金额
*

View File

@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField;
import org.springframework.util.Assert;
import java.util.List;
import static java.util.Collections.singletonList;
/**
* {@link cn.iocoder.yudao.framework.common.pojo.PageParam} 工具类
@ -60,7 +60,7 @@ public class PageUtils {
*/
public static <T> void buildDefaultSortingField(SortablePageParam sortablePageParam, Func1<T, ?> func) {
if (sortablePageParam != null && CollUtil.isEmpty(sortablePageParam.getSortingFields())) {
sortablePageParam.setSortingFields(List.of(buildSortingField(func)));
sortablePageParam.setSortingFields(singletonList(buildSortingField(func)));
}
}

View File

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-framework</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-biz-error-code</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
错误码 ErrorCode 的自动配置功能,提供如下功能:
1. 远程读取:项目启动时,从 system-server 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提示可配置;
2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-server 服务加载最新的 ErrorCode 错误码;
3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑;
</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
<version>${revision}</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided主要是 ErrorCodeProperties 使用到 -->
</dependency>
</dependencies>
</project>

View File

@ -1 +0,0 @@
cn.iocoder.yudao.framework.errorcode.config.YudaoErrorCodeAutoConfiguration

View File

@ -1,82 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-framework</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-biz-sms</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>短信拓展,支持阿里云、腾讯云</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>io.opentracing</groupId>
<artifactId>opentracing-util</artifactId> <!-- aliyun 短信需要,进行链路追踪 -->
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<optional>true</optional> <!-- 设置为可选,因为使用到 @VisibleForTesting 用于单元测试 -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<!-- 三方云服务相关 -->
<!-- SMS SDK begin -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
</dependency>
<!-- SMS SDK end -->
</dependencies>
</project>

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.framework.sms.config;
import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.framework.sms.core.client.impl.SmsClientFactoryImpl;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
* 短信配置类
*
* @author 芋道源码
*/
@AutoConfiguration
public class YudaoSmsAutoConfiguration {
@Bean
public SmsClientFactory smsClientFactory() {
return new SmsClientFactoryImpl();
}
}

View File

@ -1,50 +0,0 @@
package cn.iocoder.yudao.framework.sms.core.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
* 短信框架的错误码枚举
*
* 短信框架使用 2-001-000-000
*
* @author 芋道源码
*/
public interface SmsFrameworkErrorCodeConstants {
ErrorCode SMS_UNKNOWN = new ErrorCode(2_001_000_000, "未知错误,需要解析");
// ========== 权限 / 限流等相关 2-001-000-100 ==========
ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2_001_000_100, "没有发送短信的权限");
ErrorCode SMS_IP_DENY = new ErrorCode(2_001_000_100, "IP 不允许发送短信");
// 阿里云将短信发送频率限制在正常的业务限流范围内默认短信验证码使用同一签名对同一个手机号验证码支持 1 / 分钟5 / 小时累计 10 /
ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2_001_000_102, "指定手机的发送限流");
// 阿里云已经达到您在控制台设置的短信日发送量限额值在国内消息设置 > 安全设置修改发送总量阈值
ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2_001_000_103, "每天的发送限流");
ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2_001_000_104, "短信内容有敏感词");
// 腾讯云为避免骚扰用户营销短信只允许在8点到22点发送
ErrorCode SMS_SEND_MARKET_LIMIT_CONTROL = new ErrorCode(2_001_000_105, "营销短信发送时间限制");
// ========== 模板相关 2-001-000-200 ==========
ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2_001_000_200, "短信模板不合法"); // 包括短信模板不存在
ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2_001_000_201, "模板参数不正确");
// ========== 签名相关 2-001-000-300 ==========
ErrorCode SMS_SIGN_INVALID = new ErrorCode(2_001_000_300, "短信签名不可用");
// ========== 账户相关 2-001-000-400 ==========
ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2_001_000_400, "账户余额不足");
ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2_001_000_401, "apiKey 不存在");
// ========== 其它相关 2-001-000-900 开头 ==========
ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2_001_000_900, "请求参数缺失");
ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2_001_000_901, "手机格式不正确");
ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2_001_000_902, "手机号在黑名单中");
ErrorCode SMS_APP_ID_INVALID = new ErrorCode(2_001_000_903, "SdkAppId不合法");
ErrorCode EXCEPTION = new ErrorCode(2_001_000_999, "调用异常");
}

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>yudao-framework</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<artifactId>yudao-spring-boot-starter-desensitize</artifactId>
<description>脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,22 @@
package cn.iocoder.yudao.framework.flowable.core.enums;
/**
* 流程常量信息
*/
public interface BpmnModelConstants {
String BPMN_FILE_SUFFIX = ".bpmn";
/**
* BPMN 中的命名空间
*
* 这个东西有可能导致无法切换工作流程的实现
*/
String NAMESPACE = "http://flowable.org/bpmn";
/**
* 自定义属性 dataType
*/
String PROCESS_CUSTOM_DATA_TYPE = "dataType";
}

View File

@ -77,13 +77,6 @@
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 -->
</dependency>
<!-- TODO 芋艿:临时解决 spring boot 3.x 适配 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
</project>

View File

@ -12,7 +12,7 @@
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>Web 框架全局异常、API 日志等</description>
<description>Web 框架全局异常、API 日志、脱敏、错误码</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies>
@ -54,6 +54,11 @@
<artifactId>yudao-module-infra-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行错误码的记录 -->
<version>${revision}</version>
</dependency>
<!-- xss -->
<dependency>
@ -61,6 +66,17 @@
<artifactId>jsoup</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,4 +1,4 @@
/**
* 脱敏组件支持 JSON 返回数据时将邮箱手机等字段进行脱敏
*/
package cn.iocoder.yudao.framework.desensitize.core;
package cn.iocoder.yudao.framework.desensitize;

View File

@ -3,3 +3,5 @@ cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration
cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration
cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration
cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration
cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration
cn.iocoder.yudao.framework.errorcode.config.YudaoErrorCodeAutoConfiguration

View File

@ -1,27 +1,23 @@
package cn.iocoder.yudao.framework.desensitize.core;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.desensitize.core.annotation.Address;
import cn.iocoder.yudao.framework.desensitize.core.regex.annotation.EmailDesensitize;
import cn.iocoder.yudao.framework.desensitize.core.regex.annotation.RegexDesensitize;
import cn.iocoder.yudao.framework.desensitize.core.annotation.Address;
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.BankCardDesensitize;
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.CarLicenseDesensitize;
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.ChineseNameDesensitize;
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize;
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.IdCardDesensitize;
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.PasswordDesensitize;
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.MobileDesensitize;
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.SliderDesensitize;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.*;
import lombok.Data;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* {@link DesensitizeTest} 的单元测试
*/
public class DesensitizeTest extends BaseMockitoUnitTest {
@ExtendWith(MockitoExtension.class)
public class DesensitizeTest {
@Test
public void test() {

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.framework.desensitize.core.annotation;
import cn.iocoder.yudao.framework.desensitize.core.DesensitizeTest;
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
import cn.iocoder.yudao.framework.desensitize.core.handler.AddressHandler;
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Documented;

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.bpm.api.listener;
import cn.iocoder.yudao.module.bpm.api.listener.dto.BpmResultListenerRespDTO;
// TODO @芋艿后续改成支持 RPC
/**
* 业务流程实例的结果发生变化的监听器 Api
*
* @author HUIHUI
*/
public interface BpmResultListenerApi {
/**
* 监听的流程定义 Key
*
* @return 返回监听的流程定义 Key
*/
String getProcessDefinitionKey();
/**
* 处理事件
*
* @param event 事件
*/
void onEvent(BpmResultListenerRespDTO event);
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.bpm.api.listener.dto;
import lombok.Data;
// TODO @芋艿后续改成支持 RPC
/**
* 业务流程实例的结果 Response DTO
*
* @author HUIHUI
*/
@Data
public class BpmResultListenerRespDTO {
/**
* 流程实例的编号
*/
private String id;
/**
* 流程实例的 key
*/
private String processDefinitionKey;
/**
* 流程实例的结果
*/
private Integer result;
/**
* 流程实例对应的业务标识
* 例如说请假
*/
private String businessKey;
}

View File

@ -6,16 +6,15 @@ import cn.iocoder.yudao.framework.common.util.io.IoUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.io.IOException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;

View File

@ -49,7 +49,7 @@ public class BpmProcessInstanceCopyController {
@PostMapping("/create")
@Operation(summary = "抄送流程")
@PreAuthorize("@ss.hasPermission('bpm:process-instance-cc:create')")
public CommonResult<Boolean> createProcessInstanceCC(@Valid @RequestBody BpmProcessInstanceCopyCreateReqVO createReqVO) {
public CommonResult<Boolean> createProcessInstanceCopy(@Valid @RequestBody BpmProcessInstanceCopyCreateReqVO createReqVO) {
processInstanceCopyService.createProcessInstanceCopy(getLoginUserId(), createReqVO);
return success(true);
}
@ -57,7 +57,7 @@ public class BpmProcessInstanceCopyController {
@GetMapping("/my-page")
@Operation(summary = "获得抄送流程分页列表")
@PreAuthorize("@ss.hasPermission('bpm:process-instance-cc:query')")
public CommonResult<PageResult<BpmProcessInstanceCopyPageItemRespVO>> getProcessInstanceCCPage(
public CommonResult<PageResult<BpmProcessInstanceCopyPageItemRespVO>> getProcessInstanceCopyPage(
@Valid BpmProcessInstanceCopyMyPageReqVO pageReqVO) {
PageResult<BpmProcessInstanceCopyDO> pageResult = processInstanceCopyService.getMyProcessInstanceCopyPage(getLoginUserId(), pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {

View File

@ -1,65 +0,0 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
// TODO @kyle1明确是 Req 还是 Resp2注释可以合并到 swagger 3example 写一下这样一些 mock 接口平台可以读取 example
/**
* 流程抄送视图对象
*/
@Data
public class BpmProcessInstanceCopyVO {
/**
* 编号
*/
@Schema(description = "抄送主键")
private Long id;
/**
* 发起人Id
*/
@Schema(description = "发起人Id")
private Long startUserId;
@Schema(description = "发起人别名")
private String startUserNickname;
/**
* 流程主键
*/
@Schema(description = "流程实例的主键")
private String processInstanceId;
@Schema(description = "流程实例的名字")
private String processInstanceName;
/**
* 任务主键
*/
@Schema(description = "发起抄送的任务编号")
private String taskId;
@Schema(description = "发起抄送的任务名称")
private String taskName;
/**
* 用户主键
*/
@Schema(description = "用户编号")
private Long userId;
@Schema(description = "用户别名")
private Long userNickname;
@Schema(description = "抄送原因")
private String reason;
@Schema(description = "抄送人")
private String creator;
@Schema(description = "抄送时间")
private LocalDateTime createTime;
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.bpm.framework.bpm.listener;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.api.listener.BpmResultListenerApi;
import cn.iocoder.yudao.module.bpm.api.listener.dto.BpmResultListenerRespDTO;
import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEvent;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.util.List;
// TODO @芋艿后续改成支持 RPC
/**
* 业务流程结果监听器实现类
*
* @author HUIHUI
*/
@Component
public class BpmServiceResultListener implements ApplicationListener<BpmProcessInstanceResultEvent> {
@Resource
private List<BpmResultListenerApi> bpmResultListenerApis;
@Override
public final void onApplicationEvent(BpmProcessInstanceResultEvent event) {
bpmResultListenerApis.forEach(bpmResultListenerApi -> {
if (!StrUtil.equals(event.getProcessDefinitionKey(), bpmResultListenerApi.getProcessDefinitionKey())) {
return;
}
bpmResultListenerApi.onEvent(BeanUtils.toBean(event, BpmResultListenerRespDTO.class));
});
}
}

View File

@ -0,0 +1,76 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.handler;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.flowable.core.enums.BpmnModelConstants;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
// TODO @芋艿bpmn 分配人融合时需要搞下这块
/**
* 多实例处理类
*/
@AllArgsConstructor
@Component("multiInstanceHandler")
public class MultiInstanceHandler {
@Resource
private AdminUserApi userApi;
@Resource
private PermissionApi permissionApi;
/**
* 流程发起人那种情况不需要处理
* flowable 完成
*
* @param execution flowable的执行对象
* @return 用户ID
*/
public Set<String> getUserIds(DelegateExecution execution) {
Set<String> candidateUserIds = new LinkedHashSet<>();
FlowElement flowElement = execution.getCurrentFlowElement();
if (ObjectUtil.isNotEmpty(flowElement) && flowElement instanceof UserTask userTask) {
String dataType = userTask.getAttributeValue(BpmnModelConstants.NAMESPACE, BpmnModelConstants.PROCESS_CUSTOM_DATA_TYPE);
if ("USERS".equals(dataType) && CollUtil.isNotEmpty(userTask.getCandidateUsers())) {
// 添加候选用户id
candidateUserIds.addAll(userTask.getCandidateUsers());
} else if (CollUtil.isNotEmpty(userTask.getCandidateGroups())) {
// 获取组的ID角色ID集合或部门ID集合
List<Long> groups = userTask.getCandidateGroups().stream()
// 例如部门DEPT100100才是部门id
.map(item -> Long.parseLong(item.substring(4)))
.collect(Collectors.toList());
List<Long> userIds = new ArrayList<>();
if ("ROLES".equals(dataType)) {
// 通过角色id获取所有用户id集合
Set<Long> userRoleIdListByRoleIds = permissionApi.getUserRoleIdListByRoleIds(groups);
userIds = new ArrayList<>(userRoleIdListByRoleIds);
} else if ("DEPTS".equals(dataType)) {
// 通过部门id获取所有用户id集合
List<AdminUserRespDTO> userListByDeptIds = userApi.getUserListByDeptIds(groups);
userIds = convertList(userListByDeptIds, AdminUserRespDTO::getId);
}
// 添加候选用户id
userIds.forEach(id -> candidateUserIds.add(String.valueOf(id)));
}
}
return candidateUserIds;
}
}

View File

@ -50,4 +50,5 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent
protected void processCompleted(FlowableEngineEntityEvent event) {
processInstanceService.updateProcessInstanceExtComplete((ProcessInstance)event.getEntity());
}
}

View File

@ -2,9 +2,8 @@ package cn.iocoder.yudao.module.bpm.service.definition;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
import org.flowable.bpmn.model.BpmnModel;
import jakarta.validation.Valid;
import org.flowable.bpmn.model.BpmnModel;
/**
* Flowable流程模型接口
@ -62,7 +61,7 @@ public interface BpmModelService {
/**
* 修改模型的状态实际更新的部署的流程定义的状态
*
* @param id 编号
* @param id 编号
* @param state 状态
*/
void updateModelState(String id, Integer state);

View File

@ -14,6 +14,8 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
@ -29,8 +31,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;

View File

@ -5,8 +5,8 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.PageUtils;
import cn.iocoder.yudao.framework.flowable.core.enums.BpmnModelConstants;
import cn.iocoder.yudao.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionListReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageItemRespVO;
@ -17,6 +17,8 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessDefinitionExtMapper;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
@ -29,13 +31,10 @@ import org.flowable.engine.repository.ProcessDefinitionQuery;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_KEY_NOT_MATCH;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NAME_NOT_MATCH;
import static java.util.Collections.emptyList;
@ -53,8 +52,6 @@ import static java.util.Collections.emptyList;
@Slf4j
public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionService {
private static final String BPMN_FILE_SUFFIX = ".bpmn";
@Resource
private RepositoryService repositoryService;
@ -125,7 +122,7 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
// 创建 Deployment 部署
Deployment deploy = repositoryService.createDeployment()
.key(createReqDTO.getKey()).name(createReqDTO.getName()).category(createReqDTO.getCategory())
.addBytes(createReqDTO.getKey() + BPMN_FILE_SUFFIX, createReqDTO.getBpmnBytes())
.addBytes(createReqDTO.getKey() + BpmnModelConstants.BPMN_FILE_SUFFIX, createReqDTO.getBpmnBytes())
.tenantId(TenantContextHolder.getTenantIdStr())
.deploy();

View File

@ -16,10 +16,10 @@
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>
crm 包下客户关系管理Customer Relationship Management
例如说:客户、联系人、商机、合同、回款等等
商业智能 BI 模块,包括:报表、图表、数据大屏等等
</description>
</project>

View File

@ -11,6 +11,8 @@ public interface ErrorCodeConstants {
// ========== 合同管理 1-020-000-000 ==========
ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
ErrorCode CONTRACT_UPDATE_FAIL_EDITING_PROHIBITED = new ErrorCode(1_020_000_001, "更新合同失败,原因:禁止编辑");
ErrorCode CONTRACT_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_020_000_002, "合同提交审核失败,原因:合同没处在未提交状态");
// ========== 线索管理 1-020-001-000 ==========
ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
@ -26,8 +28,8 @@ public interface ErrorCodeConstants {
// ========== 联系人管理 1-020-003-000 ==========
ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
ErrorCode CONTACT_BUSINESS_LINK_NOT_EXISTS = new ErrorCode( 1_020_003_001, "联系人商机关联不存在");
ErrorCode CONTACT_DELETE_FAIL_CONTRACT_LINK_EXISTS = new ErrorCode( 1_020_003_002, "联系人已关联合同,不能删除");
ErrorCode CONTACT_BUSINESS_LINK_NOT_EXISTS = new ErrorCode(1_020_003_001, "联系人商机关联不存在");
ErrorCode CONTACT_DELETE_FAIL_CONTRACT_LINK_EXISTS = new ErrorCode(1_020_003_002, "联系人已关联合同,不能删除");
// ========== 回款 1-020-004-000 ==========
ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_020_004_000, "回款不存在");
@ -48,6 +50,9 @@ public interface ErrorCodeConstants {
ErrorCode CUSTOMER_LOCK_EXCEED_LIMIT = new ErrorCode(1_020_006_009, "锁定客户失败,超出锁定规则上限");
ErrorCode CUSTOMER_OWNER_EXCEED_LIMIT = new ErrorCode(1_020_006_010, "操作失败,超出客户数拥有上限");
ErrorCode CUSTOMER_DELETE_FAIL_HAVE_REFERENCE = new ErrorCode(1_020_006_011, "删除客户失败,有关联{}");
ErrorCode CUSTOMER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_020_006_012, "导入客户数据不能为空!");
ErrorCode CUSTOMER_CREATE_NAME_NOT_NULL = new ErrorCode(1_020_006_013, "客户名称不能为空!");
ErrorCode CUSTOMER_NAME_EXISTS = new ErrorCode(1_020_006_014, "已存在名为【{}】的客户!");
// ========== 权限管理 1_020_007_000 ==========
ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
@ -80,7 +85,8 @@ public interface ErrorCodeConstants {
ErrorCode BUSINESS_STATUS_NOT_EXISTS = new ErrorCode(1_020_011_000, "商机状态不存在");
// ========== 客户公海规则设置 1_020_012_000 ==========
ErrorCode CUSTOMER_LIMIT_CONFIG_NOT_EXISTS = new ErrorCode(1_020_012_000, "客户限制配置不存在");
ErrorCode CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_020_012_000, "客户公海配置不存在或未启用");
ErrorCode CUSTOMER_LIMIT_CONFIG_NOT_EXISTS = new ErrorCode(1_020_012_001, "客户限制配置不存在");
// ========== 跟进记录 1_020_013_000 ==========
ErrorCode FOLLOW_UP_RECORD_NOT_EXISTS = new ErrorCode(1_020_013_000, "跟进记录不存在");

View File

@ -39,6 +39,8 @@ public interface LogRecordConstants {
String CRM_CUSTOMER_POOL_SUCCESS = "将客户【{{#customerName}}】放入了公海";
String CRM_CUSTOMER_RECEIVE_SUB_TYPE = "{{#ownerUserName != null ? '分配客户' : '领取客户'}}";
String CRM_CUSTOMER_RECEIVE_SUCCESS = "{{#ownerUserName != null ? '将客户【' + #customer.name + '】分配给【' + #ownerUserName + '】' : '领取客户【' + #customer.name + '】'}}";
String CRM_CUSTOMER_IMPORT_SUB_TYPE = "{{#isUpdate ? '导入并更新客户' : '导入客户'}}";
String CRM_CUSTOMER_IMPORT_SUCCESS = "{{#isUpdate ? '导入并更新了客户【'+ #customer.name +'】' : '导入了客户【'+ #customer.name +'】'}}";
// ======================= CRM_CUSTOMER_LIMIT_CONFIG 客户限制配置 =======================

View File

@ -27,6 +27,11 @@
<artifactId>yudao-module-crm-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-bpm-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
@ -37,6 +42,10 @@
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>

View File

@ -0,0 +1,9 @@
### 合同金额排行榜
GET {{baseUrl}}/crm/bi-rank/get-contract-price-rank?deptId=100&times[0]=2022-12-12 00:00:00&times[1]=2024-12-12 23:59:59
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
### 回款金额排行榜
GET {{baseUrl}}/crm/bi-rank/get-receivable-price-rank?deptId=100&times[0]=2022-12-12 00:00:00&times[1]=2024-12-12 23:59:59
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}

View File

@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.crm.controller.admin.bi;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.crm.controller.admin.bi.vo.CrmBiRanKRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.bi.vo.CrmBiRankReqVO;
import cn.iocoder.yudao.module.crm.service.bi.CrmBiRankingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - CRM BI 排行榜")
@RestController
@RequestMapping("/crm/bi-rank")
@Validated
public class CrmBiRankController {
@Resource
private CrmBiRankingService rankingService;
@GetMapping("/get-contract-price-rank")
@Operation(summary = "获得合同金额排行榜")
@PreAuthorize("@ss.hasPermission('crm:bi-rank:query')")
public CommonResult<List<CrmBiRanKRespVO>> getContractPriceRank(@Valid CrmBiRankReqVO rankingReqVO) {
return success(rankingService.getContractPriceRank(rankingReqVO));
}
@GetMapping("/get-receivable-price-rank")
@Operation(summary = "获得回款金额排行榜")
@PreAuthorize("@ss.hasPermission('crm:bi-rank:query')")
public CommonResult<List<CrmBiRanKRespVO>> getReceivablePriceRank(@Valid CrmBiRankReqVO rankingReqVO) {
return success(rankingService.getReceivablePriceRank(rankingReqVO));
}
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.crm.controller.admin.bi.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - CRM BI 排行榜 Response VO")
@Data
public class CrmBiRanKRespVO {
@Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long ownerUserId;
@Schema(description = "姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private String nickname;
@Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private String deptName;
/**
* 数量是个特别抽象的概念在不同排行下代表不同含义
*
* 1. 金额合同金额排行回款金额排行
* 2. 个数签约合同排行产品销量排行产品销量排行新增客户数排行新增联系人排行跟进次数排行跟进客户数排行
*/
@Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer count;
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.crm.controller.admin.bi.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - CRM BI 排行榜 Request VO")
@Data
public class CrmBiRankReqVO {
@Schema(description = "部门 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "部门 id 不能为空")
private Long deptId;
/**
* userIds 目前不用前端传递目前是方便后端通过 deptId 读取编号后设置回来
*
* 后续可能会支持选择部分用户进行查询
*/
@Schema(description = "负责人用户 id 集合", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2")
private List<Long> userIds;
@Schema(description = "时间范围", requiredMode = Schema.RequiredMode.REQUIRED)
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@NotEmpty(message = "时间范围不能为空")
private LocalDateTime[] times;
}

View File

@ -41,7 +41,7 @@ import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.E
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
@Tag(name = "管理后台 - 商机")
@Tag(name = "管理后台 - CRM 商机")
@RestController
@RequestMapping("/crm/business")
@Validated
@ -169,7 +169,7 @@ public class CrmBusinessController {
@PutMapping("/transfer")
@Operation(summary = "商机转移")
@PreAuthorize("@ss.hasPermission('crm:business:update')")
public CommonResult<Boolean> transfer(@Valid @RequestBody CrmBusinessTransferReqVO reqVO) {
public CommonResult<Boolean> transferBusiness(@Valid @RequestBody CrmBusinessTransferReqVO reqVO) {
businessService.transferBusiness(reqVO, getLoginUserId());
return success(true);
}

View File

@ -40,7 +40,7 @@ import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Tag(name = "管理后台 - 商机状态类型")
@Tag(name = "管理后台 - CRM 商机状态类型")
@RestController
@RequestMapping("/crm/business-status-type")
@Validated

View File

@ -91,7 +91,7 @@ public class CrmClueController {
@PutMapping("/transfer")
@Operation(summary = "线索转移")
@PreAuthorize("@ss.hasPermission('crm:clue:update')")
public CommonResult<Boolean> transfer(@Valid @RequestBody CrmClueTransferReqVO reqVO) {
public CommonResult<Boolean> transferClue(@Valid @RequestBody CrmClueTransferReqVO reqVO) {
clueService.transferClue(reqVO, getLoginUserId());
return success(true);
}
@ -99,7 +99,7 @@ public class CrmClueController {
@PostMapping("/transform")
@Operation(summary = "线索转化为客户")
@PreAuthorize("@ss.hasPermission('crm:clue:update')")
public CommonResult<Boolean> translateCustomer(@Valid @RequestBody CrmClueTransformReqVO reqVO) {
public CommonResult<Boolean> translateCustomer(@Valid @RequestBody CrmClueTranslateReqVO reqVO) {
clueService.translateCustomer(reqVO, getLoginUserId());
return success(Boolean.TRUE);
}

View File

@ -30,4 +30,13 @@ public class CrmCluePageReqVO extends PageParam {
@Schema(description = "是否为公海数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
private Boolean pool; // null 则表示为不是公海数据
@Schema(description = "所属行业", example = "1")
private Integer industryId;
@Schema(description = "客户等级", example = "1")
private Integer level;
@Schema(description = "客户来源", example = "1")
private Integer source;
}

View File

@ -77,4 +77,39 @@ public class CrmClueRespVO {
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@Schema(description = "所属行业", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
@ExcelProperty(value = "所属行业", converter = DictConvert.class)
@DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY)
private Integer industryId;
@Schema(description = "客户等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
@ExcelProperty(value = "客户等级", converter = DictConvert.class)
@DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL)
private Integer level;
@Schema(description = "客户来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
@ExcelProperty(value = "客户来源", converter = DictConvert.class)
@DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE)
private Integer source;
@Schema(description = "网址", example = "25682")
@ExcelProperty("网址")
private String website;
@Schema(description = "QQ", example = "25682")
@ExcelProperty("QQ")
private String qq;
@Schema(description = "wechat", example = "25682")
@ExcelProperty("wechat")
private String wechat;
@Schema(description = "email", example = "25682")
@ExcelProperty("email")
private String email;
@Schema(description = "客户描述", example = "25682")
@ExcelProperty("客户描述")
private String description;
}

View File

@ -1,16 +1,25 @@
package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.framework.common.validation.Mobile;
import cn.iocoder.yudao.framework.common.validation.Telephone;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerLevelEnum;
import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerIndustryParseFunction;
import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerLevelParseFunction;
import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerSourceParseFunction;
import com.mzt.logapi.starter.annotation.DiffLogField;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY;
@Schema(description = "管理后台 - CRM 线索 创建/更新 Request VO")
@Data
@ -55,4 +64,42 @@ public class CrmClueSaveReqVO {
@DiffLogField(name = "备注")
private String remark;
@Schema(description = "所属行业", example = "1")
@DiffLogField(name = "所属行业", function = CrmCustomerIndustryParseFunction.NAME)
@DictFormat(CRM_CUSTOMER_INDUSTRY)
private Integer industryId;
@Schema(description = "客户等级", example = "2")
@DiffLogField(name = "客户等级", function = CrmCustomerLevelParseFunction.NAME)
@InEnum(CrmCustomerLevelEnum.class)
private Integer level;
@Schema(description = "客户来源", example = "3")
@DiffLogField(name = "客户来源", function = CrmCustomerSourceParseFunction.NAME)
private Integer source;
@Schema(description = "网址", example = "https://www.baidu.com")
@DiffLogField(name = "网址")
private String website;
@Schema(description = "QQ", example = "123456789")
@DiffLogField(name = "QQ")
@Size(max = 20, message = "QQ长度不能超过 20 个字符")
private String qq;
@Schema(description = "微信", example = "123456789")
@DiffLogField(name = "微信")
@Size(max = 255, message = "微信长度不能超过 255 个字符")
private String wechat;
@Schema(description = "邮箱", example = "123456789@qq.com")
@DiffLogField(name = "邮箱")
@Email(message = "邮箱格式不正确")
@Size(max = 255, message = "邮箱长度不能超过 255 个字符")
private String email;
@Schema(description = "客户描述", example = "任意文字")
@DiffLogField(name = "客户描述")
@Size(max = 4096, message = "客户描述长度不能超过 4096 个字符")
private String description;
}

View File

@ -8,7 +8,7 @@ import java.util.Set;
@Schema(description = "管理后台 - 线索转化为客户 Request VO")
@Data
public class CrmClueTransformReqVO {
public class CrmClueTranslateReqVO {
@Schema(description = "线索编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024, 1025]")
@NotEmpty(message = "线索编号不能为空")

View File

@ -102,7 +102,7 @@ public class CrmContactController {
List<CrmCustomerDO> customerList = customerService.getCustomerList(
Collections.singletonList(contact.getCustomerId()));
// 3. 直属上级
List<CrmContactDO> parentContactList = contactService.getContactList(
List<CrmContactDO> parentContactList = contactService.getContactListByIds(
Collections.singletonList(contact.getParentId()), getLoginUserId());
return success(CrmContactConvert.INSTANCE.convert(contact, userMap, customerList, parentContactList));
}
@ -112,7 +112,7 @@ public class CrmContactController {
@Parameter(name = "ids", description = "编号", required = true, example = "[1024]")
@PreAuthorize("@ss.hasPermission('crm:contact:query')")
public CommonResult<List<CrmContactRespVO>> getContactListByIds(@RequestParam("ids") List<Long> ids) {
return success(BeanUtils.toBean(contactService.getContactList(ids, getLoginUserId()), CrmContactRespVO.class));
return success(BeanUtils.toBean(contactService.getContactListByIds(ids, getLoginUserId()), CrmContactRespVO.class));
}
@GetMapping("/simple-all-list")
@ -170,7 +170,7 @@ public class CrmContactController {
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(contactList,
contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
// 3. 直属上级
List<CrmContactDO> parentContactList = contactService.getContactList(
List<CrmContactDO> parentContactList = contactService.getContactListByIds(
convertSet(contactList, CrmContactDO::getParentId), getLoginUserId());
return CrmContactConvert.INSTANCE.convertPage(pageResult, userMap, crmCustomerDOList, parentContactList);
}
@ -178,7 +178,7 @@ public class CrmContactController {
@PutMapping("/transfer")
@Operation(summary = "联系人转移")
@PreAuthorize("@ss.hasPermission('crm:contact:update')")
public CommonResult<Boolean> transfer(@Valid @RequestBody CrmContactTransferReqVO reqVO) {
public CommonResult<Boolean> transferContact(@Valid @RequestBody CrmContactTransferReqVO reqVO) {
contactService.transferContact(reqVO, getLoginUserId());
return success(true);
}

View File

@ -10,10 +10,18 @@ import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
import cn.iocoder.yudao.module.crm.convert.contract.CrmContractConvert;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
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.dal.dataobject.product.CrmProductDO;
import cn.iocoder.yudao.module.crm.service.business.CrmBusinessProductService;
import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import cn.iocoder.yudao.module.crm.service.product.CrmProductService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
@ -22,18 +30,20 @@ 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.*;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@ -47,6 +57,15 @@ public class CrmContractController {
private CrmContractService contractService;
@Resource
private CrmCustomerService customerService;
@Resource
private CrmContactService contactService;
@Resource
private CrmBusinessService businessService;
@Resource
@Lazy
private CrmBusinessProductService businessProductService;
@Resource
private CrmProductService productService;
@Resource
private AdminUserApi adminUserApi;
@ -80,8 +99,28 @@ public class CrmContractController {
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('crm:contract:query')")
public CommonResult<CrmContractRespVO> getContract(@RequestParam("id") Long id) {
// 1. 查询合同
CrmContractDO contract = contractService.getContract(id);
return success(BeanUtils.toBean(contract, CrmContractRespVO.class));
if (contract == null) {
return success(null);
}
// 2.1 拼接合同信息
List<CrmContractRespVO> respVOList = buildContractDetailList(Collections.singletonList(contract));
// 2.2 拼接产品信息
// TODO @puhui999下面这块也可以搞到 convert 里哈可以在 ContractDetailList 加个开关是不是查询商品信息psjdk21 的方法不太能去用因为 jdk8 项目要兼容
CrmContractRespVO respVO = respVOList.get(0);
List<CrmBusinessProductDO> businessProductList = businessProductService.getBusinessProductListByContractId(id);
Map<Long, CrmBusinessProductDO> businessProductMap = convertMap(businessProductList, CrmBusinessProductDO::getProductId);
List<CrmProductDO> productList = productService.getProductListByIds(convertSet(businessProductList, CrmBusinessProductDO::getProductId));
respVO.setProductItems(convertList(productList, product -> {
CrmContractRespVO.CrmContractProductItemRespVO productItemRespVO = BeanUtils.toBean(product, CrmContractRespVO.CrmContractProductItemRespVO.class);
findAndThen(businessProductMap, product.getId(), businessProduct -> {
productItemRespVO.setCount(businessProduct.getCount()).setDiscountPercent(businessProduct.getDiscountPercent());
});
return productItemRespVO;
}));
return success(respVO);
}
@GetMapping("/page")
@ -89,15 +128,15 @@ public class CrmContractController {
@PreAuthorize("@ss.hasPermission('crm:contract:query')")
public CommonResult<PageResult<CrmContractRespVO>> getContractPage(@Valid CrmContractPageReqVO pageVO) {
PageResult<CrmContractDO> pageResult = contractService.getContractPage(pageVO, getLoginUserId());
return success(buildContractDetailPage(pageResult));
return success(BeanUtils.toBean(pageResult, CrmContractRespVO.class).setList(buildContractDetailList(pageResult.getList())));
}
@GetMapping("/page-by-customer")
@Operation(summary = "获得联系人分页,基于指定客户")
@Operation(summary = "获得合同分页,基于指定客户")
public CommonResult<PageResult<CrmContractRespVO>> getContractPageByCustomer(@Valid CrmContractPageReqVO pageVO) {
Assert.notNull(pageVO.getCustomerId(), "客户编号不能为空");
PageResult<CrmContractDO> pageResult = contractService.getContractPageByCustomerId(pageVO);
return success(buildContractDetailPage(pageResult));
return success(BeanUtils.toBean(pageResult, CrmContractRespVO.class).setList(buildContractDetailList(pageResult.getList())));
}
@GetMapping("/export-excel")
@ -113,31 +152,44 @@ public class CrmContractController {
}
/**
* 构建详细的合同分页结果
* 构建详细的合同结果
*
* @param pageResult 简单的合同分页结果
* @return 细的合同分页结果
* @param contractList 原始合同信息
* @return 细的合同结果
*/
private PageResult<CrmContractRespVO> buildContractDetailPage(PageResult<CrmContractDO> pageResult) {
List<CrmContractDO> contactList = pageResult.getList();
if (CollUtil.isEmpty(contactList)) {
return PageResult.empty(pageResult.getTotal());
private List<CrmContractRespVO> buildContractDetailList(List<CrmContractDO> contractList) {
if (CollUtil.isEmpty(contractList)) {
return Collections.emptyList();
}
// 1. 获取客户列表
List<CrmCustomerDO> customerList = customerService.getCustomerList(
convertSet(contactList, CrmContractDO::getCustomerId));
convertSet(contractList, CrmContractDO::getCustomerId));
// 2. 获取创建人负责人列表
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(contactList,
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(contractList,
contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
return CrmContractConvert.INSTANCE.convertPage(pageResult, userMap, customerList);
// 3. 获取联系人
Map<Long, CrmContactDO> contactMap = convertMap(contactService.getContactListByIds(convertSet(contractList,
CrmContractDO::getContactId)), CrmContactDO::getId);
// 4. 获取商机
Map<Long, CrmBusinessDO> businessMap = convertMap(businessService.getBusinessList(convertSet(contractList,
CrmContractDO::getBusinessId)), CrmBusinessDO::getId);
return CrmContractConvert.INSTANCE.convertList(contractList, userMap, customerList, contactMap, businessMap);
}
@PutMapping("/transfer")
@Operation(summary = "合同转移")
@PreAuthorize("@ss.hasPermission('crm:contract:update')")
public CommonResult<Boolean> transfer(@Valid @RequestBody CrmContractTransferReqVO reqVO) {
public CommonResult<Boolean> transferContract(@Valid @RequestBody CrmContractTransferReqVO reqVO) {
contractService.transferContract(reqVO, getLoginUserId());
return success(true);
}
@PutMapping("/submit")
@Operation(summary = "提交合同审批")
@PreAuthorize("@ss.hasPermission('crm:contract:update')")
public CommonResult<Boolean> submitContract(@RequestParam("id") Long id) {
contractService.submitContract(id, getLoginUserId());
return success(true);
}
}

View File

@ -5,6 +5,7 @@ import lombok.Data;
import java.time.LocalDateTime;
// TODO @puhui999合并到 RespVO 里哈
/**
* CRM 合同 Excel VO
*

View File

@ -3,10 +3,13 @@ package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ -26,10 +29,16 @@ public class CrmContractRespVO {
@Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18336")
@ExcelProperty("客户编号")
private Long customerId;
@Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "18336")
@ExcelProperty("客户名称")
private String customerName;
@Schema(description = "商机编号", example = "10864")
@ExcelProperty("商机编号")
private Long businessId;
@Schema(description = "商机名称", example = "10864")
@ExcelProperty("商机名称")
private String businessName;
@Schema(description = "工作流编号", example = "1043")
@ExcelProperty("工作流编号")
@ -74,10 +83,16 @@ public class CrmContractRespVO {
@Schema(description = "联系人编号", example = "18546")
@ExcelProperty("联系人编号")
private Long contactId;
@Schema(description = "联系人编号", example = "18546")
@ExcelProperty("联系人编号")
private String contactName;
@Schema(description = "公司签约人", example = "14036")
@ExcelProperty("公司签约人")
private Long signUserId;
@Schema(description = "公司签约人", example = "14036")
@ExcelProperty("公司签约人")
private String signUserName;
@Schema(description = "最后跟进时间")
@ExcelProperty("最后跟进时间")
@ -101,9 +116,10 @@ public class CrmContractRespVO {
@ExcelProperty("创建人名字")
private String creatorName;
@Schema(description = "客户名字", example = "test")
@ExcelProperty("客户名字")
private String customerName;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("更新时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime updateTime;
@Schema(description = "负责人", example = "test")
@ExcelProperty("负责人")
@ -113,4 +129,36 @@ public class CrmContractRespVO {
@ExcelProperty("审批状态")
private Integer auditStatus;
@Schema(description = "产品列表")
private List<CrmContractProductItemRespVO> productItems;
@Schema(description = "产品列表")
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class CrmContractProductItemRespVO {
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
private Long id;
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是产品")
private String name;
@Schema(description = "产品编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "N881")
private String no;
@Schema(description = "单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer unit;
@Schema(description = "价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer price;
@Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
private Integer count;
@Schema(description = "产品折扣", example = "99")
private Integer discountPercent;
}
}

View File

@ -7,10 +7,13 @@ import cn.iocoder.yudao.module.crm.framework.operatelog.core.SysAdminUserParseFu
import com.mzt.logapi.starter.annotation.DiffLogField;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ -35,10 +38,6 @@ public class CrmContractSaveReqVO {
@DiffLogField(name = "商机", function = CrmBusinessParseFunction.NAME)
private Long businessId;
@Schema(description = "工作流编号", example = "1043")
@DiffLogField(name = "工作流编号")
private Long processInstanceId;
@Schema(description = "下单日期", requiredMode = Schema.RequiredMode.REQUIRED)
@DiffLogField(name = "下单日期")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ -86,16 +85,31 @@ public class CrmContractSaveReqVO {
@DiffLogField(name = "公司签约人", function = SysAdminUserParseFunction.NAME)
private Long signUserId;
@Schema(description = "最后跟进时间")
@DiffLogField(name = "最后跟进时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime contactLastTime;
@Schema(description = "备注", example = "你猜")
@DiffLogField(name = "备注")
private String remark;
// TODO @dhb52增加一个 status 字段具体有哪些值你来枚举下主要页面上有个草稿提交审核的流程可以看看然后要对接工作流这块也可以看看不确定的地方问我
@Schema(description = "产品列表")
private List<CrmContractProductItem> productItems;
@Schema(description = "产品列表")
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class CrmContractProductItem {
@Schema(description = "产品编号", example = "20529")
@NotNull(message = "产品编号不能为空")
private Long id;
@Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
@NotNull(message = "产品数量不能为空")
private Integer count;
@Schema(description = "产品折扣")
private Integer discountPercent;
}
}

View File

@ -21,6 +21,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@ -29,18 +30,22 @@ import org.mapstruct.ap.internal.util.Collections;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED;
@Tag(name = "管理后台 - CRM 客户")
@RestController
@ -57,7 +62,6 @@ public class CrmCustomerController {
@Resource
private AdminUserApi adminUserApi;
@PostMapping("/create")
@Operation(summary = "创建客户")
@PreAuthorize("@ss.hasPermission('crm:customer:create')")
@ -118,6 +122,34 @@ public class CrmCustomerController {
return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult, userMap, deptMap, poolDayMap));
}
@GetMapping("/put-in-pool-remind-page")
@Operation(summary = "获得待进入公海客户分页")
@PreAuthorize("@ss.hasPermission('crm:customer:query')")
public CommonResult<PageResult<CrmCustomerRespVO>> getPutInPoolRemindCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
// 获取公海配置 TODO @dbh52合并到 getPutInPoolRemindCustomerPage 会更合适哈
CrmCustomerPoolConfigDO poolConfigDO = customerPoolConfigService.getCustomerPoolConfig();
if (ObjUtil.isNull(poolConfigDO)
|| Boolean.FALSE.equals(poolConfigDO.getEnabled())
|| Boolean.FALSE.equals(poolConfigDO.getNotifyEnabled())
) { // TODO @dbh52这个括号一般不换行 java 这里
throw exception(CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED);
}
// 1. 查询客户分页
PageResult<CrmCustomerDO> pageResult = customerService.getPutInPoolRemindCustomerPage(pageVO, poolConfigDO, getLoginUserId());
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2. 拼接数据
// TODO @芋艿合并 getCustomerPage getPutInPoolRemindCustomerPage 的后置处理
Map<Long, Long> poolDayMap = getPoolDayMap(pageResult.getList()); // 客户界面需要查看距离进入公海的时间
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSetByFlatMap(pageResult.getList(), user -> Stream.of(Long.parseLong(user.getCreator()), user.getOwnerUserId())));
Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult, userMap, deptMap, poolDayMap));
}
/**
* 获取距离进入公海的时间
*
@ -156,6 +188,7 @@ public class CrmCustomerController {
new CrmCustomerRespVO().setId(customer.getId()).setName(customer.getName())));
}
// TODO @puhui999公海的导出前端可以接下
@GetMapping("/export-excel")
@Operation(summary = "导出客户 Excel")
@PreAuthorize("@ss.hasPermission('crm:customer:export')")
@ -169,10 +202,41 @@ public class CrmCustomerController {
BeanUtils.toBean(list, CrmCustomerRespVO.class));
}
@GetMapping("/get-import-template")
@Operation(summary = "获得导入客户模板")
public void importTemplate(HttpServletResponse response) throws IOException {
// 手动创建导出 demo
List<CrmCustomerImportExcelVO> list = Arrays.asList(
CrmCustomerImportExcelVO.builder().name("芋道").industryId(1).level(1).source(1).mobile("15601691300").telephone("")
.website("https://doc.iocoder.cn/").qq("").wechat("").email("yunai@iocoder.cn").description("").remark("")
.areaId(null).detailAddress("").build(),
CrmCustomerImportExcelVO.builder().name("源码").industryId(1).level(1).source(1).mobile("15601691300").telephone("")
.website("https://doc.iocoder.cn/").qq("").wechat("").email("yunai@iocoder.cn").description("").remark("")
.areaId(null).detailAddress("").build()
);
// 输出
ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list);
}
// TODO @puhui999updateSupport 要不改成前端必须传递哈哈哈代码排版看着有点乱
// TODO @puhui999加一个选择负责人允许空空就进入公海
@PostMapping("/import")
@Operation(summary = "导入客户")
@Parameters({
@Parameter(name = "file", description = "Excel 文件", required = true),
@Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
})
@PreAuthorize("@ss.hasPermission('system:customer:import')")
public CommonResult<CrmCustomerImportRespVO> importExcel(@RequestParam("file") MultipartFile file, @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport)
throws Exception {
List<CrmCustomerImportExcelVO> list = ExcelUtils.read(file, CrmCustomerImportExcelVO.class);
return success(customerService.importCustomerList(list, updateSupport, getLoginUserId()));
}
@PutMapping("/transfer")
@Operation(summary = "转移客户")
@PreAuthorize("@ss.hasPermission('crm:customer:update')")
public CommonResult<Boolean> transfer(@Valid @RequestBody CrmCustomerTransferReqVO reqVO) {
public CommonResult<Boolean> transferCustomer(@Valid @RequestBody CrmCustomerTransferReqVO reqVO) {
customerService.transferCustomer(reqVO, getLoginUserId());
return success(true);
}

View File

@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.*;
/**
* 客户 Excel 导入 VO
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = false) // 设置 chain = false避免用户导入有问题
public class CrmCustomerImportExcelVO {
@ExcelProperty("客户名称")
private String name;
// TODO @puhui999industryIdlevelsource 字段可以研究下怎么搞下拉框
@ExcelProperty(value = "所属行业", converter = DictConvert.class)
@DictFormat(CRM_CUSTOMER_INDUSTRY)
private Integer industryId;
@ExcelProperty(value = "客户等级", converter = DictConvert.class)
@DictFormat(CRM_CUSTOMER_LEVEL)
private Integer level;
@ExcelProperty(value = "客户来源", converter = DictConvert.class)
@DictFormat(CRM_CUSTOMER_SOURCE)
private Integer source;
@ExcelProperty("手机")
private String mobile;
@ExcelProperty("电话")
private String telephone;
@ExcelProperty("网址")
private String website;
@ExcelProperty("QQ")
private String qq;
@ExcelProperty("微信")
private String wechat;
@ExcelProperty("邮箱")
private String email;
@ExcelProperty("客户描述")
private String description;
@ExcelProperty("备注")
private String remark;
// TODO @puhui999需要选择省市区需要研究下怎么搞合理点
@ExcelProperty("地区编号")
private Integer areaId;
@ExcelProperty("详细地址")
private String detailAddress;
}

View File

@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - 客户导入 Response VO")
@Data
@Builder
public class CrmCustomerImportRespVO {
@Schema(description = "创建成功的客户名数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> createCustomerNames;
@Schema(description = "更新成功的客户名数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> updateCustomerNames;
@Schema(description = "导入失败的客户集合key 为客户名value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, String> failureCustomerNames;
}

View File

@ -75,7 +75,7 @@ public class CrmFollowUpRecordController {
public CommonResult<PageResult<CrmFollowUpRecordRespVO>> getFollowUpRecordPage(@Valid CrmFollowUpRecordPageReqVO pageReqVO) {
PageResult<CrmFollowUpRecordDO> pageResult = followUpRecordService.getFollowUpRecordPage(pageReqVO);
/// 拼接数据
Map<Long, CrmContactDO> contactMap = convertMap(contactService.getContactList(
Map<Long, CrmContactDO> contactMap = convertMap(contactService.getContactListByIds(
convertSetByFlatMap(pageResult.getList(), item -> item.getContactIds().stream())), CrmContactDO::getId);
Map<Long, CrmBusinessDO> businessMap = convertMap(businessService.getBusinessList(
convertSetByFlatMap(pageResult.getList(), item -> item.getBusinessIds().stream())), CrmBusinessDO::getId);

View File

@ -2,14 +2,14 @@ package cn.iocoder.yudao.module.crm.controller.admin.operatelog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo.CrmOperateLogPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo.CrmOperateLogV2RespVO;
import cn.iocoder.yudao.module.crm.enums.LogRecordConstants;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2PageReqDTO;
import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2RespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
@ -51,16 +51,14 @@ public class CrmOperateLogController {
BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(), CRM_RECEIVABLE_PLAN_TYPE);
}
// TODO @puhui999还是搞个 VO 出来哈
@GetMapping("/page")
@Operation(summary = "获得操作日志")
@Parameter(name = "id", description = "客户编号", required = true)
@PreAuthorize("@ss.hasPermission('crm:customer:query')")
public CommonResult<PageResult<OperateLogV2RespDTO>> getCustomerOperateLog(@Valid CrmOperateLogPageReqVO pageReqVO) {
@PreAuthorize("@ss.hasPermission('crm:operate-log:query')")
public CommonResult<PageResult<CrmOperateLogV2RespVO>> getCustomerOperateLog(@Valid CrmOperateLogPageReqVO pageReqVO) {
OperateLogV2PageReqDTO reqDTO = new OperateLogV2PageReqDTO();
reqDTO.setPageSize(PAGE_SIZE_NONE); // 默认不分页需要分页需注释
reqDTO.setBizType(BIZ_TYPE_MAP.get(pageReqVO.getBizType())).setBizId(pageReqVO.getBizId());
return success(operateLogApi.getOperateLogPage(reqDTO));
return success(BeanUtils.toBean(operateLogApi.getOperateLogPage(reqDTO), CrmOperateLogV2RespVO.class));
}
}

View File

@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - CRM 跟进 Response VO")
@Data
@ExcelIgnoreUnannotated
public class CrmOperateLogV2RespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
private Long id;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long userId;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String userName;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer userType;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
private String type;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "修改客户")
private String subType;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
private Long bizId;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "将什么从什么改为了什么")
private String action;
@Schema(description = "编号", example = "{orderId: 1}")
private String extra;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-01-01")
private LocalDateTime createTime;
}

View File

@ -28,6 +28,7 @@ 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.*;
@ -55,6 +56,7 @@ public class CrmReceivablePlanController {
@Resource
private CrmReceivableService receivableService;
@Resource
@Lazy
private CrmContractService contractService;
@Resource
private CrmCustomerService customerService;

View File

@ -1,9 +1,10 @@
package cn.iocoder.yudao.module.crm.convert.contract;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractTransferReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
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.service.permission.bo.CrmPermissionTransferReqBO;
@ -31,17 +32,21 @@ public interface CrmContractConvert {
@Mapping(target = "bizId", source = "reqVO.id")
CrmPermissionTransferReqBO convert(CrmContractTransferReqVO reqVO, Long userId);
default PageResult<CrmContractRespVO> convertPage(PageResult<CrmContractDO> pageResult, Map<Long, AdminUserRespDTO> userMap,
List<CrmCustomerDO> customerList) {
PageResult<CrmContractRespVO> voPageResult = BeanUtils.toBean(pageResult, CrmContractRespVO.class);
default List<CrmContractRespVO> convertList(List<CrmContractDO> contractList, Map<Long, AdminUserRespDTO> userMap,
List<CrmCustomerDO> customerList, Map<Long, CrmContactDO> contactMap,
Map<Long, CrmBusinessDO> businessMap) {
List<CrmContractRespVO> respVOList = BeanUtils.toBean(contractList, CrmContractRespVO.class);
// 拼接关联字段
Map<Long, CrmCustomerDO> customerMap = convertMap(customerList, CrmCustomerDO::getId);
voPageResult.getList().forEach(contract -> {
respVOList.forEach(contract -> {
findAndThen(userMap, contract.getOwnerUserId(), user -> contract.setOwnerUserName(user.getNickname()));
findAndThen(userMap, Long.parseLong(contract.getCreator()), user -> contract.setCreatorName(user.getNickname()));
findAndThen(userMap, contract.getSignUserId(), user -> contract.setSignUserName(user.getNickname()));
findAndThen(customerMap, contract.getCustomerId(), customer -> contract.setCustomerName(customer.getName()));
findAndThen(contactMap, contract.getContactId(), contact -> contract.setContactName(contact.getName()));
findAndThen(businessMap, contract.getBusinessId(), business -> contract.setBusinessName(business.getName()));
});
return voPageResult;
return respVOList;
}
}

Some files were not shown because too many files have changed in this diff Show More