kangwenjing 2026-03-20 16:39:07 +08:00
parent aa427a2e06
commit 9f4d30f933
149 changed files with 10667 additions and 798 deletions

BIN
.DS_Store vendored

Binary file not shown.

0
.idea/.gitignore vendored 100644
View File

18
.idea/compiler.xml 100644
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="unis-crm-backend" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="unis-crm-backend" options="-parameters" />
</option>
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/backend/src/main/java" charset="UTF-8" />
</component>
</project>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

14
.idea/misc.xml 100644
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/backend/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="openjdk-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/unis_crm.iml" filepath="$PROJECT_DIR$/.idea/unis_crm.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml 100644
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="4c558d98-824e-4a48-ba48-bd2e6172f9f4" name="更改" comment="">
<change beforePath="$PROJECT_DIR$/backend/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/backend/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/dist/assets/index-DTZ3L0iU.js" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/dist/assets/index-cLTs2L9U.css" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/dist/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/dist/index.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/index.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/App.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/App.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/index.css" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/index.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/pages/Dashboard.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/pages/Dashboard.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/pages/Expansion.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/pages/Expansion.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/pages/Profile.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/pages/Profile.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/vite.config.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/vite.config.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 1
}]]></component>
<component name="ProjectId" id="3BBm14kQhaD2gQxOS8rBU8WsdoX" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"WebServerToolWindowFactoryState": "false",
"git-widget-placeholder": "main",
"last_opened_file_path": "/Users/kangwenjing/Downloads/crm/unis_crm",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"project.structure.last.edited": "Libraries",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.0",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RunManager">
<configuration name="UnisCrmBackendApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<module name="unis-crm-backend" />
<option name="SPRING_BOOT_MAIN_CLASS" value="com.unis.crm.UnisCrmBackendApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="应用程序级" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="4c558d98-824e-4a48-ba48-bd2e6172f9f4" name="更改" comment="" />
<created>1773970620258</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1773970620258</updated>
<workItem from="1773970621225" duration="1084000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

3
.vscode/settings.json vendored 100644
View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}

View File

@ -1,17 +1,65 @@
# Backend
该目录用于存放后端代码,当前仅完成结构预留,方便后续继续开发。
当前已经补齐首页所需的 Java 后端基础工程,并调整为常规 Spring Boot 单体项目目录结构,技术栈如下:
建议后续可按下面方式扩展:
- Java 17
- Spring Boot 3.2.2
- MyBatis Plus 3.5.6
- PostgreSQL
- Redis
## 已实现内容
- 首页聚合接口:`GET /api/dashboard/home`
- 首页欢迎信息:姓名、职位、部门、入职天数
- 首页统计卡片
- 首页待办列表
- 首页最新动态
- 通用响应体与全局异常处理
## 目录结构
```text
backend/
├── src/
│ ├── controllers/
│ ├── routes/
│ ├── services/
│ ├── models/
│ └── app.ts
├── package.json
└── README.md
├── pom.xml
└── src/main
├── java/com/unis/crm
│ ├── common
│ ├── controller
│ ├── dto
│ ├── mapper
│ ├── service
│ └── UnisCrmBackendApplication.java
└── resources
├── application.yml
└── mapper/dashboard
```
## 启动前准备
1. 执行数据库脚本:
```bash
psql -h 127.0.0.1 -U postgres -d nex_auth -f sql/init_pg17.sql
```
2. 确保 `sys_user`、`work_todo`、`sys_activity_log`、`crm_customer`、`crm_opportunity`、`work_checkin` 中有业务数据。
## 启动项目
```bash
cd backend
mvn spring-boot:run
```
默认启动在 `8081` 端口,供前端开发环境通过 Vite 代理访问;`8080` 可继续保留给现有认证/系统服务。
## 首页接口
请求示例:
```bash
curl -H "X-User-Id: 1" "http://127.0.0.1:8081/api/dashboard/home"
```
首页接口只允许查询当前登录用户自己的数据,必须通过 `X-User-Id` 传入当前用户ID不支持指定其他用户查询。

1
backend/cp.txt 100644

File diff suppressed because one or more lines are too long

80
backend/pom.xml 100644
View File

@ -0,0 +1,80 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
<relativePath/>
</parent>
<groupId>com.unis.crm</groupId>
<artifactId>unis-crm-backend</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>unis-crm-backend</name>
<description>UNIS CRM backend</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.unisbase</groupId>
<artifactId>unisbase-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.unis.crm.UnisCrmBackendApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,14 @@
package com.unis.crm;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "com.unis.crm")
@MapperScan("com.unis.crm.mapper")
public class UnisCrmBackendApplication {
public static void main(String[] args) {
SpringApplication.run(UnisCrmBackendApplication.class, args);
}
}

View File

@ -0,0 +1,49 @@
package com.unis.crm.common;
public class ApiResponse<T> {
private String code;
private String msg;
private T data;
public ApiResponse() {
}
public ApiResponse(String code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>("0", "success", data);
}
public static <T> ApiResponse<T> fail(String msg) {
return new ApiResponse<>("-1", msg, null);
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}

View File

@ -0,0 +1,8 @@
package com.unis.crm.common;
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}

View File

@ -0,0 +1,43 @@
package com.unis.crm.common;
import jakarta.servlet.http.HttpServletRequest;
import java.time.OffsetDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class CrmGlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Object> handleBusinessException(BusinessException ex) {
return ApiResponse.fail(ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Object> handleValidationException(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.findFirst()
.map(error -> error.getField() + " " + error.getDefaultMessage())
.orElse("请求参数校验失败");
return ApiResponse.fail(message);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> handleUnexpectedException(Exception ex, HttpServletRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", OffsetDateTime.now().toString());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("error", HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
body.put("message", ex.getMessage());
body.put("path", request.getRequestURI());
return body;
}
}

View File

@ -0,0 +1,14 @@
package com.unis.crm.common;
public final class CurrentUserUtils {
private CurrentUserUtils() {
}
public static Long requireCurrentUserId(Long headerUserId) {
if (headerUserId == null || headerUserId <= 0) {
throw new BusinessException("未识别到当前登录用户");
}
return headerUserId;
}
}

View File

@ -0,0 +1,29 @@
package com.unis.crm.controller;
import com.unis.crm.common.ApiResponse;
import com.unis.crm.dto.dashboard.DashboardHomeDTO;
import com.unis.crm.service.DashboardService;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequestMapping("/api/dashboard")
public class DashboardController {
private final DashboardService dashboardService;
public DashboardController(DashboardService dashboardService) {
this.dashboardService = dashboardService;
}
@GetMapping("/home")
public ApiResponse<DashboardHomeDTO> getHome(
@RequestHeader("X-User-Id") @Min(1) Long userId) {
return ApiResponse.success(dashboardService.getHome(userId));
}
}

View File

@ -0,0 +1,87 @@
package com.unis.crm.controller;
import com.unis.crm.common.ApiResponse;
import com.unis.crm.common.CurrentUserUtils;
import com.unis.crm.dto.expansion.CreateChannelExpansionRequest;
import com.unis.crm.dto.expansion.CreateExpansionFollowUpRequest;
import com.unis.crm.dto.expansion.CreateSalesExpansionRequest;
import com.unis.crm.dto.expansion.ExpansionMetaDTO;
import com.unis.crm.dto.expansion.ExpansionOverviewDTO;
import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest;
import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest;
import com.unis.crm.service.ExpansionService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/expansion")
public class ExpansionController {
private final ExpansionService expansionService;
public ExpansionController(ExpansionService expansionService) {
this.expansionService = expansionService;
}
@GetMapping("/meta")
public ApiResponse<ExpansionMetaDTO> getMeta(@RequestHeader("X-User-Id") Long userId) {
CurrentUserUtils.requireCurrentUserId(userId);
return ApiResponse.success(expansionService.getMeta());
}
@GetMapping("/overview")
public ApiResponse<ExpansionOverviewDTO> getOverview(
@RequestHeader("X-User-Id") Long userId,
@RequestParam(value = "keyword", required = false) String keyword) {
return ApiResponse.success(expansionService.getOverview(CurrentUserUtils.requireCurrentUserId(userId), keyword));
}
@PostMapping("/sales")
public ApiResponse<Long> createSales(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody CreateSalesExpansionRequest request) {
return ApiResponse.success(expansionService.createSalesExpansion(CurrentUserUtils.requireCurrentUserId(userId), request));
}
@PostMapping("/channel")
public ApiResponse<Long> createChannel(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody CreateChannelExpansionRequest request) {
return ApiResponse.success(expansionService.createChannelExpansion(CurrentUserUtils.requireCurrentUserId(userId), request));
}
@PutMapping("/sales/{id}")
public ApiResponse<Void> updateSales(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("id") Long id,
@Valid @RequestBody UpdateSalesExpansionRequest request) {
expansionService.updateSalesExpansion(CurrentUserUtils.requireCurrentUserId(userId), id, request);
return ApiResponse.success(null);
}
@PutMapping("/channel/{id}")
public ApiResponse<Void> updateChannel(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("id") Long id,
@Valid @RequestBody UpdateChannelExpansionRequest request) {
expansionService.updateChannelExpansion(CurrentUserUtils.requireCurrentUserId(userId), id, request);
return ApiResponse.success(null);
}
@PostMapping("/{bizType}/{bizId}/followups")
public ApiResponse<Long> createFollowUp(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("bizType") String bizType,
@PathVariable("bizId") Long bizId,
@Valid @RequestBody CreateExpansionFollowUpRequest request) {
return ApiResponse.success(expansionService.createFollowUp(CurrentUserUtils.requireCurrentUserId(userId), bizType, bizId, request));
}
}

View File

@ -0,0 +1,60 @@
package com.unis.crm.controller;
import com.unis.crm.common.ApiResponse;
import com.unis.crm.common.CurrentUserUtils;
import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest;
import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
import com.unis.crm.dto.opportunity.OpportunityOverviewDTO;
import com.unis.crm.service.OpportunityService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/opportunities")
public class OpportunityController {
private final OpportunityService opportunityService;
public OpportunityController(OpportunityService opportunityService) {
this.opportunityService = opportunityService;
}
@GetMapping("/overview")
public ApiResponse<OpportunityOverviewDTO> getOverview(
@RequestHeader("X-User-Id") Long userId,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "stage", required = false) String stage) {
return ApiResponse.success(opportunityService.getOverview(CurrentUserUtils.requireCurrentUserId(userId), keyword, stage));
}
@PostMapping
public ApiResponse<Long> createOpportunity(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody CreateOpportunityRequest request) {
return ApiResponse.success(opportunityService.createOpportunity(CurrentUserUtils.requireCurrentUserId(userId), request));
}
@PutMapping("/{opportunityId}")
public ApiResponse<Long> updateOpportunity(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("opportunityId") Long opportunityId,
@Valid @RequestBody CreateOpportunityRequest request) {
return ApiResponse.success(opportunityService.updateOpportunity(CurrentUserUtils.requireCurrentUserId(userId), opportunityId, request));
}
@PostMapping("/{opportunityId}/followups")
public ApiResponse<Long> createFollowUp(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("opportunityId") Long opportunityId,
@Valid @RequestBody CreateOpportunityFollowUpRequest request) {
return ApiResponse.success(opportunityService.createFollowUp(CurrentUserUtils.requireCurrentUserId(userId), opportunityId, request));
}
}

View File

@ -0,0 +1,58 @@
package com.unis.crm.controller;
import com.unis.crm.common.ApiResponse;
import com.unis.crm.common.CurrentUserUtils;
import com.unis.crm.dto.work.CreateWorkCheckInRequest;
import com.unis.crm.dto.work.CreateWorkDailyReportRequest;
import com.unis.crm.dto.work.WorkOverviewDTO;
import com.unis.crm.service.WorkService;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.Valid;
import java.math.BigDecimal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/work")
public class WorkController {
private final WorkService workService;
public WorkController(WorkService workService) {
this.workService = workService;
}
@GetMapping("/overview")
public ApiResponse<WorkOverviewDTO> getOverview(@RequestHeader("X-User-Id") Long userId) {
return ApiResponse.success(workService.getOverview(CurrentUserUtils.requireCurrentUserId(userId)));
}
@GetMapping("/reverse-geocode")
public ApiResponse<String> reverseGeocode(
@RequestHeader("X-User-Id") Long userId,
@RequestParam("lat") @DecimalMin(value = "-90.0") @DecimalMax(value = "90.0") BigDecimal latitude,
@RequestParam("lon") @DecimalMin(value = "-180.0") @DecimalMax(value = "180.0") BigDecimal longitude) {
CurrentUserUtils.requireCurrentUserId(userId);
return ApiResponse.success(workService.resolveLocationName(latitude, longitude));
}
@PostMapping("/checkins")
public ApiResponse<Long> saveCheckIn(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody CreateWorkCheckInRequest request) {
return ApiResponse.success(workService.saveCheckIn(CurrentUserUtils.requireCurrentUserId(userId), request));
}
@PostMapping("/daily-reports")
public ApiResponse<Long> saveDailyReport(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody CreateWorkDailyReportRequest request) {
return ApiResponse.success(workService.saveDailyReport(CurrentUserUtils.requireCurrentUserId(userId), request));
}
}

View File

@ -0,0 +1,97 @@
package com.unis.crm.dto.dashboard;
import java.time.OffsetDateTime;
public class DashboardActivityDTO {
private Long id;
private String bizType;
private Long bizId;
private String actionType;
private String title;
private String content;
private Long operatorUserId;
private String operatorName;
private OffsetDateTime createdAt;
private String timeText;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getBizType() {
return bizType;
}
public void setBizType(String bizType) {
this.bizType = bizType;
}
public Long getBizId() {
return bizId;
}
public void setBizId(Long bizId) {
this.bizId = bizId;
}
public String getActionType() {
return actionType;
}
public void setActionType(String actionType) {
this.actionType = actionType;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Long getOperatorUserId() {
return operatorUserId;
}
public void setOperatorUserId(Long operatorUserId) {
this.operatorUserId = operatorUserId;
}
public String getOperatorName() {
return operatorName;
}
public void setOperatorName(String operatorName) {
this.operatorName = operatorName;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public String getTimeText() {
return timeText;
}
public void setTimeText(String timeText) {
this.timeText = timeText;
}
}

View File

@ -0,0 +1,95 @@
package com.unis.crm.dto.dashboard;
import java.util.List;
public class DashboardHomeDTO {
private Long userId;
private String realName;
private String jobTitle;
private String deptName;
private Long onboardingDays;
private List<DashboardStatDTO> stats;
private List<DashboardTodoDTO> todos;
private List<DashboardActivityDTO> activities;
public DashboardHomeDTO() {
}
public DashboardHomeDTO(Long userId, String realName, String jobTitle, String deptName, Long onboardingDays,
List<DashboardStatDTO> stats, List<DashboardTodoDTO> todos,
List<DashboardActivityDTO> activities) {
this.userId = userId;
this.realName = realName;
this.jobTitle = jobTitle;
this.deptName = deptName;
this.onboardingDays = onboardingDays;
this.stats = stats;
this.todos = todos;
this.activities = activities;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public String getJobTitle() {
return jobTitle;
}
public void setJobTitle(String jobTitle) {
this.jobTitle = jobTitle;
}
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
public Long getOnboardingDays() {
return onboardingDays;
}
public void setOnboardingDays(Long onboardingDays) {
this.onboardingDays = onboardingDays;
}
public List<DashboardStatDTO> getStats() {
return stats;
}
public void setStats(List<DashboardStatDTO> stats) {
this.stats = stats;
}
public List<DashboardTodoDTO> getTodos() {
return todos;
}
public void setTodos(List<DashboardTodoDTO> todos) {
this.todos = todos;
}
public List<DashboardActivityDTO> getActivities() {
return activities;
}
public void setActivities(List<DashboardActivityDTO> activities) {
this.activities = activities;
}
}

View File

@ -0,0 +1,32 @@
package com.unis.crm.dto.dashboard;
public class DashboardStatDTO {
private String name;
private Long value;
private String metricKey;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getValue() {
return value;
}
public void setValue(Long value) {
this.value = value;
}
public String getMetricKey() {
return metricKey;
}
public void setMetricKey(String metricKey) {
this.metricKey = metricKey;
}
}

View File

@ -0,0 +1,79 @@
package com.unis.crm.dto.dashboard;
import java.time.OffsetDateTime;
public class DashboardTodoDTO {
private Long id;
private String title;
private String bizType;
private Long bizId;
private String priority;
private String status;
private OffsetDateTime dueDate;
private OffsetDateTime createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getBizType() {
return bizType;
}
public void setBizType(String bizType) {
this.bizType = bizType;
}
public Long getBizId() {
return bizId;
}
public void setBizId(Long bizId) {
this.bizId = bizId;
}
public String getPriority() {
return priority;
}
public void setPriority(String priority) {
this.priority = priority;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public OffsetDateTime getDueDate() {
return dueDate;
}
public void setDueDate(OffsetDateTime dueDate) {
this.dueDate = dueDate;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@ -0,0 +1,52 @@
package com.unis.crm.dto.dashboard;
import java.time.LocalDate;
public class UserWelcomeDTO {
private Long userId;
private String realName;
private String jobTitle;
private String deptName;
private LocalDate hireDate;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public String getJobTitle() {
return jobTitle;
}
public void setJobTitle(String jobTitle) {
this.jobTitle = jobTitle;
}
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
public LocalDate getHireDate() {
return hireDate;
}
public void setHireDate(LocalDate hireDate) {
this.hireDate = hireDate;
}
}

View File

@ -0,0 +1,161 @@
package com.unis.crm.dto.expansion;
import java.util.ArrayList;
import java.util.List;
public class ChannelExpansionItemDTO {
private Long id;
private String type;
private String name;
private String province;
private String industry;
private String annualRevenue;
private String revenue;
private Integer size;
private String contact;
private String contactTitle;
private String phone;
private String stageCode;
private String stage;
private Boolean landed;
private String expectedSignDate;
private String notes;
private List<ExpansionFollowUpDTO> followUps = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
public String getAnnualRevenue() {
return annualRevenue;
}
public void setAnnualRevenue(String annualRevenue) {
this.annualRevenue = annualRevenue;
}
public String getRevenue() {
return revenue;
}
public void setRevenue(String revenue) {
this.revenue = revenue;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
public String getContact() {
return contact;
}
public void setContact(String contact) {
this.contact = contact;
}
public String getContactTitle() {
return contactTitle;
}
public void setContactTitle(String contactTitle) {
this.contactTitle = contactTitle;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getStageCode() {
return stageCode;
}
public void setStageCode(String stageCode) {
this.stageCode = stageCode;
}
public String getStage() {
return stage;
}
public void setStage(String stage) {
this.stage = stage;
}
public Boolean getLanded() {
return landed;
}
public void setLanded(Boolean landed) {
this.landed = landed;
}
public String getExpectedSignDate() {
return expectedSignDate;
}
public void setExpectedSignDate(String expectedSignDate) {
this.expectedSignDate = expectedSignDate;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public List<ExpansionFollowUpDTO> getFollowUps() {
return followUps;
}
public void setFollowUps(List<ExpansionFollowUpDTO> followUps) {
this.followUps = followUps;
}
}

View File

@ -0,0 +1,131 @@
package com.unis.crm.dto.expansion;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
public class CreateChannelExpansionRequest {
private Long id;
@NotBlank(message = "渠道名称不能为空")
@Size(max = 200, message = "渠道名称不能超过200字符")
private String channelName;
private String province;
private String industry;
private BigDecimal annualRevenue;
private Integer staffSize;
private String contactName;
private String contactTitle;
private String contactMobile;
private String stage;
private Boolean landedFlag;
private LocalDate expectedSignDate;
private String remark;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getChannelName() {
return channelName;
}
public void setChannelName(String channelName) {
this.channelName = channelName;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
public BigDecimal getAnnualRevenue() {
return annualRevenue;
}
public void setAnnualRevenue(BigDecimal annualRevenue) {
this.annualRevenue = annualRevenue;
}
public Integer getStaffSize() {
return staffSize;
}
public void setStaffSize(Integer staffSize) {
this.staffSize = staffSize;
}
public String getContactName() {
return contactName;
}
public void setContactName(String contactName) {
this.contactName = contactName;
}
public String getContactTitle() {
return contactTitle;
}
public void setContactTitle(String contactTitle) {
this.contactTitle = contactTitle;
}
public String getContactMobile() {
return contactMobile;
}
public void setContactMobile(String contactMobile) {
this.contactMobile = contactMobile;
}
public String getStage() {
return stage;
}
public void setStage(String stage) {
this.stage = stage;
}
public Boolean getLandedFlag() {
return landedFlag;
}
public void setLandedFlag(Boolean landedFlag) {
this.landedFlag = landedFlag;
}
public LocalDate getExpectedSignDate() {
return expectedSignDate;
}
public void setExpectedSignDate(LocalDate expectedSignDate) {
this.expectedSignDate = expectedSignDate;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}

View File

@ -0,0 +1,51 @@
package com.unis.crm.dto.expansion;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.OffsetDateTime;
public class CreateExpansionFollowUpRequest {
@NotBlank(message = "跟进类型不能为空")
private String followUpType;
@NotBlank(message = "跟进内容不能为空")
private String content;
private String nextAction;
@NotNull(message = "跟进时间不能为空")
private OffsetDateTime followUpTime;
public String getFollowUpType() {
return followUpType;
}
public void setFollowUpType(String followUpType) {
this.followUpType = followUpType;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getNextAction() {
return nextAction;
}
public void setNextAction(String nextAction) {
this.nextAction = nextAction;
}
public OffsetDateTime getFollowUpTime() {
return followUpTime;
}
public void setFollowUpTime(OffsetDateTime followUpTime) {
this.followUpTime = followUpTime;
}
}

View File

@ -0,0 +1,139 @@
package com.unis.crm.dto.expansion;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
public class CreateSalesExpansionRequest {
private Long id;
@NotBlank(message = "候选人姓名不能为空")
@Size(max = 50, message = "候选人姓名不能超过50字符")
private String candidateName;
private String mobile;
private String email;
private Long targetDeptId;
private String industry;
private String title;
private String intentLevel;
private String stage;
private Boolean hasDesktopExp;
private Boolean inProgress;
private String employmentStatus;
private LocalDate expectedJoinDate;
private String remark;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCandidateName() {
return candidateName;
}
public void setCandidateName(String candidateName) {
this.candidateName = candidateName;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Long getTargetDeptId() {
return targetDeptId;
}
public void setTargetDeptId(Long targetDeptId) {
this.targetDeptId = targetDeptId;
}
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getIntentLevel() {
return intentLevel;
}
public void setIntentLevel(String intentLevel) {
this.intentLevel = intentLevel;
}
public String getStage() {
return stage;
}
public void setStage(String stage) {
this.stage = stage;
}
public Boolean getHasDesktopExp() {
return hasDesktopExp;
}
public void setHasDesktopExp(Boolean hasDesktopExp) {
this.hasDesktopExp = hasDesktopExp;
}
public Boolean getInProgress() {
return inProgress;
}
public void setInProgress(Boolean inProgress) {
this.inProgress = inProgress;
}
public String getEmploymentStatus() {
return employmentStatus;
}
public void setEmploymentStatus(String employmentStatus) {
this.employmentStatus = employmentStatus;
}
public LocalDate getExpectedJoinDate() {
return expectedJoinDate;
}
public void setExpectedJoinDate(LocalDate expectedJoinDate) {
this.expectedJoinDate = expectedJoinDate;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}

View File

@ -0,0 +1,23 @@
package com.unis.crm.dto.expansion;
public class DepartmentOptionDTO {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@ -0,0 +1,79 @@
package com.unis.crm.dto.expansion;
import java.time.OffsetDateTime;
public class ExpansionFollowUpDTO {
private Long id;
private Long bizId;
private String bizType;
private OffsetDateTime followUpTime;
private String date;
private String type;
private String content;
private String user;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getBizId() {
return bizId;
}
public void setBizId(Long bizId) {
this.bizId = bizId;
}
public String getBizType() {
return bizType;
}
public void setBizType(String bizType) {
this.bizType = bizType;
}
public OffsetDateTime getFollowUpTime() {
return followUpTime;
}
public void setFollowUpTime(OffsetDateTime followUpTime) {
this.followUpTime = followUpTime;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
}

View File

@ -0,0 +1,23 @@
package com.unis.crm.dto.expansion;
import java.util.List;
public class ExpansionMetaDTO {
private List<DepartmentOptionDTO> departments;
public ExpansionMetaDTO() {
}
public ExpansionMetaDTO(List<DepartmentOptionDTO> departments) {
this.departments = departments;
}
public List<DepartmentOptionDTO> getDepartments() {
return departments;
}
public void setDepartments(List<DepartmentOptionDTO> departments) {
this.departments = departments;
}
}

View File

@ -0,0 +1,33 @@
package com.unis.crm.dto.expansion;
import java.util.List;
public class ExpansionOverviewDTO {
private List<SalesExpansionItemDTO> salesItems;
private List<ChannelExpansionItemDTO> channelItems;
public ExpansionOverviewDTO() {
}
public ExpansionOverviewDTO(List<SalesExpansionItemDTO> salesItems, List<ChannelExpansionItemDTO> channelItems) {
this.salesItems = salesItems;
this.channelItems = channelItems;
}
public List<SalesExpansionItemDTO> getSalesItems() {
return salesItems;
}
public void setSalesItems(List<SalesExpansionItemDTO> salesItems) {
this.salesItems = salesItems;
}
public List<ChannelExpansionItemDTO> getChannelItems() {
return channelItems;
}
public void setChannelItems(List<ChannelExpansionItemDTO> channelItems) {
this.channelItems = channelItems;
}
}

View File

@ -0,0 +1,188 @@
package com.unis.crm.dto.expansion;
import java.util.ArrayList;
import java.util.List;
public class SalesExpansionItemDTO {
private Long id;
private String type;
private String name;
private String phone;
private String email;
private Long targetDeptId;
private String dept;
private String industry;
private String title;
private String intentLevel;
private String intent;
private String stageCode;
private String stage;
private Boolean hasExp;
private Boolean inProgress;
private Boolean active;
private String employmentStatus;
private String expectedJoinDate;
private String notes;
private List<ExpansionFollowUpDTO> followUps = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Long getTargetDeptId() {
return targetDeptId;
}
public void setTargetDeptId(Long targetDeptId) {
this.targetDeptId = targetDeptId;
}
public String getDept() {
return dept;
}
public void setDept(String dept) {
this.dept = dept;
}
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getIntentLevel() {
return intentLevel;
}
public void setIntentLevel(String intentLevel) {
this.intentLevel = intentLevel;
}
public String getIntent() {
return intent;
}
public void setIntent(String intent) {
this.intent = intent;
}
public String getStageCode() {
return stageCode;
}
public void setStageCode(String stageCode) {
this.stageCode = stageCode;
}
public String getStage() {
return stage;
}
public void setStage(String stage) {
this.stage = stage;
}
public Boolean getHasExp() {
return hasExp;
}
public void setHasExp(Boolean hasExp) {
this.hasExp = hasExp;
}
public Boolean getInProgress() {
return inProgress;
}
public void setInProgress(Boolean inProgress) {
this.inProgress = inProgress;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
public String getEmploymentStatus() {
return employmentStatus;
}
public void setEmploymentStatus(String employmentStatus) {
this.employmentStatus = employmentStatus;
}
public String getExpectedJoinDate() {
return expectedJoinDate;
}
public void setExpectedJoinDate(String expectedJoinDate) {
this.expectedJoinDate = expectedJoinDate;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public List<ExpansionFollowUpDTO> getFollowUps() {
return followUps;
}
public void setFollowUps(List<ExpansionFollowUpDTO> followUps) {
this.followUps = followUps;
}
}

View File

@ -0,0 +1,121 @@
package com.unis.crm.dto.expansion;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
public class UpdateChannelExpansionRequest {
@NotBlank(message = "渠道名称不能为空")
@Size(max = 200, message = "渠道名称不能超过200字符")
private String channelName;
private String province;
private String industry;
private BigDecimal annualRevenue;
private Integer staffSize;
private String contactName;
private String contactTitle;
private String contactMobile;
private String stage;
private Boolean landedFlag;
private LocalDate expectedSignDate;
private String remark;
public String getChannelName() {
return channelName;
}
public void setChannelName(String channelName) {
this.channelName = channelName;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
public BigDecimal getAnnualRevenue() {
return annualRevenue;
}
public void setAnnualRevenue(BigDecimal annualRevenue) {
this.annualRevenue = annualRevenue;
}
public Integer getStaffSize() {
return staffSize;
}
public void setStaffSize(Integer staffSize) {
this.staffSize = staffSize;
}
public String getContactName() {
return contactName;
}
public void setContactName(String contactName) {
this.contactName = contactName;
}
public String getContactTitle() {
return contactTitle;
}
public void setContactTitle(String contactTitle) {
this.contactTitle = contactTitle;
}
public String getContactMobile() {
return contactMobile;
}
public void setContactMobile(String contactMobile) {
this.contactMobile = contactMobile;
}
public String getStage() {
return stage;
}
public void setStage(String stage) {
this.stage = stage;
}
public Boolean getLandedFlag() {
return landedFlag;
}
public void setLandedFlag(Boolean landedFlag) {
this.landedFlag = landedFlag;
}
public LocalDate getExpectedSignDate() {
return expectedSignDate;
}
public void setExpectedSignDate(LocalDate expectedSignDate) {
this.expectedSignDate = expectedSignDate;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}

View File

@ -0,0 +1,129 @@
package com.unis.crm.dto.expansion;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
public class UpdateSalesExpansionRequest {
@NotBlank(message = "候选人姓名不能为空")
@Size(max = 50, message = "候选人姓名不能超过50字符")
private String candidateName;
private String mobile;
private String email;
private Long targetDeptId;
private String industry;
private String title;
private String intentLevel;
private String stage;
private Boolean hasDesktopExp;
private Boolean inProgress;
private String employmentStatus;
private LocalDate expectedJoinDate;
private String remark;
public String getCandidateName() {
return candidateName;
}
public void setCandidateName(String candidateName) {
this.candidateName = candidateName;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Long getTargetDeptId() {
return targetDeptId;
}
public void setTargetDeptId(Long targetDeptId) {
this.targetDeptId = targetDeptId;
}
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getIntentLevel() {
return intentLevel;
}
public void setIntentLevel(String intentLevel) {
this.intentLevel = intentLevel;
}
public String getStage() {
return stage;
}
public void setStage(String stage) {
this.stage = stage;
}
public Boolean getHasDesktopExp() {
return hasDesktopExp;
}
public void setHasDesktopExp(Boolean hasDesktopExp) {
this.hasDesktopExp = hasDesktopExp;
}
public Boolean getInProgress() {
return inProgress;
}
public void setInProgress(Boolean inProgress) {
this.inProgress = inProgress;
}
public String getEmploymentStatus() {
return employmentStatus;
}
public void setEmploymentStatus(String employmentStatus) {
this.employmentStatus = employmentStatus;
}
public LocalDate getExpectedJoinDate() {
return expectedJoinDate;
}
public void setExpectedJoinDate(LocalDate expectedJoinDate) {
this.expectedJoinDate = expectedJoinDate;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}

View File

@ -0,0 +1,51 @@
package com.unis.crm.dto.opportunity;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.OffsetDateTime;
public class CreateOpportunityFollowUpRequest {
@NotBlank(message = "跟进类型不能为空")
private String followUpType;
@NotBlank(message = "跟进内容不能为空")
private String content;
private String nextAction;
@NotNull(message = "跟进时间不能为空")
private OffsetDateTime followUpTime;
public String getFollowUpType() {
return followUpType;
}
public void setFollowUpType(String followUpType) {
this.followUpType = followUpType;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getNextAction() {
return nextAction;
}
public void setNextAction(String nextAction) {
this.nextAction = nextAction;
}
public OffsetDateTime getFollowUpTime() {
return followUpTime;
}
public void setFollowUpTime(OffsetDateTime followUpTime) {
this.followUpTime = followUpTime;
}
}

View File

@ -0,0 +1,135 @@
package com.unis.crm.dto.opportunity;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
public class CreateOpportunityRequest {
private Long id;
@NotBlank(message = "商机名称不能为空")
@Size(max = 200, message = "商机名称不能超过200字符")
private String opportunityName;
@NotBlank(message = "客户名称不能为空")
@Size(max = 200, message = "客户名称不能超过200字符")
private String customerName;
@NotNull(message = "商机金额不能为空")
private BigDecimal amount;
private LocalDate expectedCloseDate;
@NotNull(message = "把握度不能为空")
@Min(value = 0, message = "把握度不能低于0")
@Max(value = 100, message = "把握度不能高于100")
private Integer confidencePct;
private String stage;
private String opportunityType;
private String productType;
private String source;
private Boolean pushedToOms;
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOpportunityName() {
return opportunityName;
}
public void setOpportunityName(String opportunityName) {
this.opportunityName = opportunityName;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public LocalDate getExpectedCloseDate() {
return expectedCloseDate;
}
public void setExpectedCloseDate(LocalDate expectedCloseDate) {
this.expectedCloseDate = expectedCloseDate;
}
public Integer getConfidencePct() {
return confidencePct;
}
public void setConfidencePct(Integer confidencePct) {
this.confidencePct = confidencePct;
}
public String getStage() {
return stage;
}
public void setStage(String stage) {
this.stage = stage;
}
public String getOpportunityType() {
return opportunityType;
}
public void setOpportunityType(String opportunityType) {
this.opportunityType = opportunityType;
}
public String getProductType() {
return productType;
}
public void setProductType(String productType) {
this.productType = productType;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public Boolean getPushedToOms() {
return pushedToOms;
}
public void setPushedToOms(Boolean pushedToOms) {
this.pushedToOms = pushedToOms;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@ -0,0 +1,59 @@
package com.unis.crm.dto.opportunity;
public class OpportunityFollowUpDTO {
private Long id;
private Long opportunityId;
private String date;
private String type;
private String content;
private String user;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getOpportunityId() {
return opportunityId;
}
public void setOpportunityId(Long opportunityId) {
this.opportunityId = opportunityId;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
}

View File

@ -0,0 +1,144 @@
package com.unis.crm.dto.opportunity;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class OpportunityItemDTO {
private Long id;
private String code;
private String name;
private String client;
private String owner;
private BigDecimal amount;
private String date;
private Integer confidence;
private String stage;
private String type;
private Boolean pushedToOms;
private String product;
private String source;
private String notes;
private List<OpportunityFollowUpDTO> followUps = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClient() {
return client;
}
public void setClient(String client) {
this.client = client;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public Integer getConfidence() {
return confidence;
}
public void setConfidence(Integer confidence) {
this.confidence = confidence;
}
public String getStage() {
return stage;
}
public void setStage(String stage) {
this.stage = stage;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Boolean getPushedToOms() {
return pushedToOms;
}
public void setPushedToOms(Boolean pushedToOms) {
this.pushedToOms = pushedToOms;
}
public String getProduct() {
return product;
}
public void setProduct(String product) {
this.product = product;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public List<OpportunityFollowUpDTO> getFollowUps() {
return followUps;
}
public void setFollowUps(List<OpportunityFollowUpDTO> followUps) {
this.followUps = followUps;
}
}

View File

@ -0,0 +1,23 @@
package com.unis.crm.dto.opportunity;
import java.util.List;
public class OpportunityOverviewDTO {
private List<OpportunityItemDTO> items;
public OpportunityOverviewDTO() {
}
public OpportunityOverviewDTO(List<OpportunityItemDTO> items) {
this.items = items;
}
public List<OpportunityItemDTO> getItems() {
return items;
}
public void setItems(List<OpportunityItemDTO> items) {
this.items = items;
}
}

View File

@ -0,0 +1,50 @@
package com.unis.crm.dto.work;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
public class CreateWorkCheckInRequest {
@NotBlank(message = "打卡地点不能为空")
@Size(max = 500, message = "打卡地点不能超过500字符")
private String locationText;
@Size(max = 500, message = "备注不能超过500字符")
private String remark;
private BigDecimal longitude;
private BigDecimal latitude;
public String getLocationText() {
return locationText;
}
public void setLocationText(String locationText) {
this.locationText = locationText;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public BigDecimal getLongitude() {
return longitude;
}
public void setLongitude(BigDecimal longitude) {
this.longitude = longitude;
}
public BigDecimal getLatitude() {
return latitude;
}
public void setLatitude(BigDecimal latitude) {
this.latitude = latitude;
}
}

View File

@ -0,0 +1,42 @@
package com.unis.crm.dto.work;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class CreateWorkDailyReportRequest {
@NotBlank(message = "今日工作内容不能为空")
@Size(max = 4000, message = "今日工作内容不能超过4000字符")
private String workContent;
@NotBlank(message = "明日工作计划不能为空")
@Size(max = 4000, message = "明日工作计划不能超过4000字符")
private String tomorrowPlan;
@Size(max = 50, message = "来源类型不能超过50字符")
private String sourceType;
public String getWorkContent() {
return workContent;
}
public void setWorkContent(String workContent) {
this.workContent = workContent;
}
public String getTomorrowPlan() {
return tomorrowPlan;
}
public void setTomorrowPlan(String tomorrowPlan) {
this.tomorrowPlan = tomorrowPlan;
}
public String getSourceType() {
return sourceType;
}
public void setSourceType(String sourceType) {
this.sourceType = sourceType;
}
}

View File

@ -0,0 +1,79 @@
package com.unis.crm.dto.work;
import java.math.BigDecimal;
public class WorkCheckInDTO {
private Long id;
private String date;
private String time;
private String locationText;
private String remark;
private String status;
private BigDecimal longitude;
private BigDecimal latitude;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
public String getLocationText() {
return locationText;
}
public void setLocationText(String locationText) {
this.locationText = locationText;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public BigDecimal getLongitude() {
return longitude;
}
public void setLongitude(BigDecimal longitude) {
this.longitude = longitude;
}
public BigDecimal getLatitude() {
return latitude;
}
public void setLatitude(BigDecimal latitude) {
this.latitude = latitude;
}
}

View File

@ -0,0 +1,86 @@
package com.unis.crm.dto.work;
public class WorkDailyReportDTO {
private Long id;
private String date;
private String submitTime;
private String workContent;
private String tomorrowPlan;
private String sourceType;
private String status;
private Integer score;
private String comment;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getSubmitTime() {
return submitTime;
}
public void setSubmitTime(String submitTime) {
this.submitTime = submitTime;
}
public String getWorkContent() {
return workContent;
}
public void setWorkContent(String workContent) {
this.workContent = workContent;
}
public String getTomorrowPlan() {
return tomorrowPlan;
}
public void setTomorrowPlan(String tomorrowPlan) {
this.tomorrowPlan = tomorrowPlan;
}
public String getSourceType() {
return sourceType;
}
public void setSourceType(String sourceType) {
this.sourceType = sourceType;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Integer getScore() {
return score;
}
public void setScore(Integer score) {
this.score = score;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
}

View File

@ -0,0 +1,77 @@
package com.unis.crm.dto.work;
public class WorkHistoryItemDTO {
private Long id;
private String type;
private String date;
private String time;
private String content;
private String status;
private Integer score;
private String comment;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Integer getScore() {
return score;
}
public void setScore(Integer score) {
this.score = score;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
}

View File

@ -0,0 +1,57 @@
package com.unis.crm.dto.work;
import java.util.List;
public class WorkOverviewDTO {
private WorkCheckInDTO todayCheckIn;
private WorkDailyReportDTO todayReport;
private String suggestedWorkContent;
private List<WorkHistoryItemDTO> history;
public WorkOverviewDTO() {
}
public WorkOverviewDTO(
WorkCheckInDTO todayCheckIn,
WorkDailyReportDTO todayReport,
String suggestedWorkContent,
List<WorkHistoryItemDTO> history) {
this.todayCheckIn = todayCheckIn;
this.todayReport = todayReport;
this.suggestedWorkContent = suggestedWorkContent;
this.history = history;
}
public WorkCheckInDTO getTodayCheckIn() {
return todayCheckIn;
}
public void setTodayCheckIn(WorkCheckInDTO todayCheckIn) {
this.todayCheckIn = todayCheckIn;
}
public WorkDailyReportDTO getTodayReport() {
return todayReport;
}
public void setTodayReport(WorkDailyReportDTO todayReport) {
this.todayReport = todayReport;
}
public String getSuggestedWorkContent() {
return suggestedWorkContent;
}
public void setSuggestedWorkContent(String suggestedWorkContent) {
this.suggestedWorkContent = suggestedWorkContent;
}
public List<WorkHistoryItemDTO> getHistory() {
return history;
}
public void setHistory(List<WorkHistoryItemDTO> history) {
this.history = history;
}
}

View File

@ -0,0 +1,23 @@
package com.unis.crm.mapper;
import com.unis.crm.dto.dashboard.DashboardActivityDTO;
import com.unis.crm.dto.dashboard.DashboardStatDTO;
import com.unis.crm.dto.dashboard.DashboardTodoDTO;
import com.unis.crm.dto.dashboard.UserWelcomeDTO;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface DashboardMapper {
Long selectDefaultUserId();
UserWelcomeDTO selectUserWelcome(@Param("userId") Long userId);
List<DashboardStatDTO> selectDashboardStats(@Param("userId") Long userId);
List<DashboardTodoDTO> selectPendingTodos(@Param("userId") Long userId);
List<DashboardActivityDTO> selectLatestActivities(@Param("userId") Long userId);
}

View File

@ -0,0 +1,46 @@
package com.unis.crm.mapper;
import com.unis.crm.dto.expansion.ChannelExpansionItemDTO;
import com.unis.crm.dto.expansion.CreateChannelExpansionRequest;
import com.unis.crm.dto.expansion.CreateExpansionFollowUpRequest;
import com.unis.crm.dto.expansion.CreateSalesExpansionRequest;
import com.unis.crm.dto.expansion.DepartmentOptionDTO;
import com.unis.crm.dto.expansion.ExpansionFollowUpDTO;
import com.unis.crm.dto.expansion.SalesExpansionItemDTO;
import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest;
import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ExpansionMapper {
List<DepartmentOptionDTO> selectDepartments();
List<SalesExpansionItemDTO> selectSalesExpansions(@Param("userId") Long userId, @Param("keyword") String keyword);
List<ChannelExpansionItemDTO> selectChannelExpansions(@Param("userId") Long userId, @Param("keyword") String keyword);
List<ExpansionFollowUpDTO> selectSalesFollowUps(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
List<ExpansionFollowUpDTO> selectChannelFollowUps(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
int insertSalesExpansion(@Param("userId") Long userId, @Param("request") CreateSalesExpansionRequest request);
int insertChannelExpansion(@Param("userId") Long userId, @Param("request") CreateChannelExpansionRequest request);
int updateSalesExpansion(@Param("userId") Long userId, @Param("id") Long id, @Param("request") UpdateSalesExpansionRequest request);
int updateChannelExpansion(@Param("userId") Long userId, @Param("id") Long id, @Param("request") UpdateChannelExpansionRequest request);
int countOwnedSalesExpansion(@Param("userId") Long userId, @Param("id") Long id);
int countOwnedChannelExpansion(@Param("userId") Long userId, @Param("id") Long id);
int insertExpansionFollowUp(
@Param("bizType") String bizType,
@Param("bizId") Long bizId,
@Param("userId") Long userId,
@Param("request") CreateExpansionFollowUpRequest request);
}

View File

@ -0,0 +1,48 @@
package com.unis.crm.mapper;
import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest;
import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO;
import com.unis.crm.dto.opportunity.OpportunityItemDTO;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OpportunityMapper {
List<OpportunityItemDTO> selectOpportunities(
@Param("userId") Long userId,
@Param("keyword") String keyword,
@Param("stage") String stage);
List<OpportunityFollowUpDTO> selectOpportunityFollowUps(
@Param("userId") Long userId,
@Param("opportunityIds") List<Long> opportunityIds);
Long selectOwnedCustomerIdByName(@Param("userId") Long userId, @Param("customerName") String customerName);
int insertCustomer(
@Param("id") Long id,
@Param("userId") Long userId,
@Param("customerName") String customerName,
@Param("source") String source);
int insertOpportunity(
@Param("userId") Long userId,
@Param("customerId") Long customerId,
@Param("request") CreateOpportunityRequest request);
int countOwnedOpportunity(@Param("userId") Long userId, @Param("id") Long id);
int updateOpportunity(
@Param("userId") Long userId,
@Param("opportunityId") Long opportunityId,
@Param("customerId") Long customerId,
@Param("request") CreateOpportunityRequest request);
int insertOpportunityFollowUp(
@Param("userId") Long userId,
@Param("opportunityId") Long opportunityId,
@Param("request") CreateOpportunityFollowUpRequest request);
}

View File

@ -0,0 +1,47 @@
package com.unis.crm.mapper;
import com.unis.crm.dto.work.CreateWorkCheckInRequest;
import com.unis.crm.dto.work.CreateWorkDailyReportRequest;
import com.unis.crm.dto.work.WorkCheckInDTO;
import com.unis.crm.dto.work.WorkDailyReportDTO;
import com.unis.crm.dto.work.WorkHistoryItemDTO;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface WorkMapper {
WorkCheckInDTO selectTodayCheckIn(@Param("userId") Long userId);
WorkDailyReportDTO selectTodayReport(@Param("userId") Long userId);
List<String> selectTodayWorkContentLines(@Param("userId") Long userId);
List<WorkHistoryItemDTO> selectHistory(@Param("userId") Long userId);
Long selectTodayCheckInId(@Param("userId") Long userId);
int insertCheckIn(@Param("userId") Long userId, @Param("request") CreateWorkCheckInRequest request);
int updateCheckIn(@Param("checkInId") Long checkInId, @Param("request") CreateWorkCheckInRequest request);
Long selectTodayReportId(@Param("userId") Long userId);
int insertDailyReport(@Param("userId") Long userId, @Param("request") CreateWorkDailyReportRequest request);
int updateDailyReport(@Param("reportId") Long reportId, @Param("request") CreateWorkDailyReportRequest request);
Long selectTodoIdByBiz(@Param("userId") Long userId, @Param("bizType") String bizType, @Param("bizId") Long bizId);
int insertTodo(
@Param("todoId") Long todoId,
@Param("userId") Long userId,
@Param("title") String title,
@Param("bizType") String bizType,
@Param("bizId") Long bizId);
int updateTodo(
@Param("todoId") Long todoId,
@Param("title") String title);
}

View File

@ -0,0 +1,8 @@
package com.unis.crm.service;
import com.unis.crm.dto.dashboard.DashboardHomeDTO;
public interface DashboardService {
DashboardHomeDTO getHome(Long userId);
}

View File

@ -0,0 +1,26 @@
package com.unis.crm.service;
import com.unis.crm.dto.expansion.CreateChannelExpansionRequest;
import com.unis.crm.dto.expansion.CreateExpansionFollowUpRequest;
import com.unis.crm.dto.expansion.CreateSalesExpansionRequest;
import com.unis.crm.dto.expansion.ExpansionMetaDTO;
import com.unis.crm.dto.expansion.ExpansionOverviewDTO;
import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest;
import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest;
public interface ExpansionService {
ExpansionMetaDTO getMeta();
ExpansionOverviewDTO getOverview(Long userId, String keyword);
Long createSalesExpansion(Long userId, CreateSalesExpansionRequest request);
Long createChannelExpansion(Long userId, CreateChannelExpansionRequest request);
void updateSalesExpansion(Long userId, Long id, UpdateSalesExpansionRequest request);
void updateChannelExpansion(Long userId, Long id, UpdateChannelExpansionRequest request);
Long createFollowUp(Long userId, String bizType, Long bizId, CreateExpansionFollowUpRequest request);
}

View File

@ -0,0 +1,16 @@
package com.unis.crm.service;
import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest;
import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
import com.unis.crm.dto.opportunity.OpportunityOverviewDTO;
public interface OpportunityService {
OpportunityOverviewDTO getOverview(Long userId, String keyword, String stage);
Long createOpportunity(Long userId, CreateOpportunityRequest request);
Long updateOpportunity(Long userId, Long opportunityId, CreateOpportunityRequest request);
Long createFollowUp(Long userId, Long opportunityId, CreateOpportunityFollowUpRequest request);
}

View File

@ -0,0 +1,17 @@
package com.unis.crm.service;
import com.unis.crm.dto.work.CreateWorkCheckInRequest;
import com.unis.crm.dto.work.CreateWorkDailyReportRequest;
import com.unis.crm.dto.work.WorkOverviewDTO;
import java.math.BigDecimal;
public interface WorkService {
WorkOverviewDTO getOverview(Long userId);
Long saveCheckIn(Long userId, CreateWorkCheckInRequest request);
Long saveDailyReport(Long userId, CreateWorkDailyReportRequest request);
String resolveLocationName(BigDecimal latitude, BigDecimal longitude);
}

View File

@ -0,0 +1,102 @@
package com.unis.crm.service.impl;
import com.unis.crm.common.BusinessException;
import com.unis.crm.dto.dashboard.DashboardActivityDTO;
import com.unis.crm.dto.dashboard.DashboardHomeDTO;
import com.unis.crm.dto.dashboard.DashboardStatDTO;
import com.unis.crm.dto.dashboard.DashboardTodoDTO;
import com.unis.crm.dto.dashboard.UserWelcomeDTO;
import com.unis.crm.mapper.DashboardMapper;
import com.unis.crm.service.DashboardService;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class DashboardServiceImpl implements DashboardService {
private final DashboardMapper dashboardMapper;
public DashboardServiceImpl(DashboardMapper dashboardMapper) {
this.dashboardMapper = dashboardMapper;
}
@Override
public DashboardHomeDTO getHome(Long userId) {
if (userId == null) {
throw new BusinessException("未获取到当前登录用户,禁止查询他人数据");
}
UserWelcomeDTO user = dashboardMapper.selectUserWelcome(userId);
if (user == null) {
throw new BusinessException("未找到当前用户对应数据");
}
List<DashboardStatDTO> stats = dashboardMapper.selectDashboardStats(userId);
List<DashboardTodoDTO> todos = dashboardMapper.selectPendingTodos(userId);
List<DashboardActivityDTO> activities = dashboardMapper.selectLatestActivities(userId);
enrichActivityTimeText(activities);
long onboardingDays = 0;
LocalDate hireDate = user.getHireDate();
if (hireDate != null) {
onboardingDays = ChronoUnit.DAYS.between(hireDate, LocalDate.now()) + 1;
}
return new DashboardHomeDTO(
user.getUserId(),
user.getRealName(),
user.getJobTitle(),
user.getDeptName(),
onboardingDays,
stats,
todos,
activities
);
}
private void enrichActivityTimeText(List<DashboardActivityDTO> activities) {
if (activities == null || activities.isEmpty()) {
return;
}
OffsetDateTime now = OffsetDateTime.now();
for (DashboardActivityDTO activity : activities) {
activity.setTimeText(formatRelativeTime(activity.getCreatedAt(), now));
}
}
private String formatRelativeTime(OffsetDateTime createdAt, OffsetDateTime now) {
if (createdAt == null) {
return "";
}
long minutes = ChronoUnit.MINUTES.between(createdAt, now);
if (minutes < 1) {
return "刚刚";
}
if (minutes < 60) {
return minutes + "分钟前";
}
long hours = ChronoUnit.HOURS.between(createdAt, now);
if (hours < 24) {
return hours + "小时前";
}
LocalDate createdDate = createdAt.atZoneSameInstant(ZoneId.systemDefault()).toLocalDate();
LocalDate today = now.atZoneSameInstant(ZoneId.systemDefault()).toLocalDate();
long days = ChronoUnit.DAYS.between(createdDate, today);
if (days == 1) {
return "昨天";
}
if (days < 7) {
return days + "天前";
}
return createdAt.toLocalDate().toString();
}
}

View File

@ -0,0 +1,248 @@
package com.unis.crm.service.impl;
import com.unis.crm.common.BusinessException;
import com.unis.crm.dto.expansion.ChannelExpansionItemDTO;
import com.unis.crm.dto.expansion.CreateChannelExpansionRequest;
import com.unis.crm.dto.expansion.CreateExpansionFollowUpRequest;
import com.unis.crm.dto.expansion.CreateSalesExpansionRequest;
import com.unis.crm.dto.expansion.ExpansionMetaDTO;
import com.unis.crm.dto.expansion.ExpansionFollowUpDTO;
import com.unis.crm.dto.expansion.ExpansionOverviewDTO;
import com.unis.crm.dto.expansion.SalesExpansionItemDTO;
import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest;
import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest;
import com.unis.crm.mapper.ExpansionMapper;
import com.unis.crm.service.ExpansionService;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class ExpansionServiceImpl implements ExpansionService {
private static final DateTimeFormatter FOLLOW_UP_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private static final Logger log = LoggerFactory.getLogger(ExpansionServiceImpl.class);
private final ExpansionMapper expansionMapper;
public ExpansionServiceImpl(ExpansionMapper expansionMapper) {
this.expansionMapper = expansionMapper;
}
@Override
public ExpansionMetaDTO getMeta() {
try {
return new ExpansionMetaDTO(expansionMapper.selectDepartments());
} catch (Exception ex) {
log.warn("Failed to load expansion departments, fallback to empty list", ex);
return new ExpansionMetaDTO(Collections.emptyList());
}
}
@Override
public ExpansionOverviewDTO getOverview(Long userId, String keyword) {
String normalizedKeyword = normalizeKeyword(keyword);
List<SalesExpansionItemDTO> salesItems = expansionMapper.selectSalesExpansions(userId, normalizedKeyword);
List<ChannelExpansionItemDTO> channelItems = expansionMapper.selectChannelExpansions(userId, normalizedKeyword);
attachSalesFollowUps(userId, salesItems);
attachChannelFollowUps(userId, channelItems);
return new ExpansionOverviewDTO(salesItems, channelItems);
}
@Override
public Long createSalesExpansion(Long userId, CreateSalesExpansionRequest request) {
fillSalesDefaults(request);
expansionMapper.insertSalesExpansion(userId, request);
if (request.getId() == null) {
throw new BusinessException("销售拓展新增失败");
}
return request.getId();
}
@Override
public Long createChannelExpansion(Long userId, CreateChannelExpansionRequest request) {
fillChannelDefaults(request);
expansionMapper.insertChannelExpansion(userId, request);
if (request.getId() == null) {
throw new BusinessException("渠道拓展新增失败");
}
return request.getId();
}
@Override
public void updateSalesExpansion(Long userId, Long id, UpdateSalesExpansionRequest request) {
fillSalesDefaults(request);
int updated = expansionMapper.updateSalesExpansion(userId, id, request);
if (updated <= 0) {
throw new BusinessException("未找到可编辑的销售拓展记录");
}
}
@Override
public void updateChannelExpansion(Long userId, Long id, UpdateChannelExpansionRequest request) {
fillChannelDefaults(request);
int updated = expansionMapper.updateChannelExpansion(userId, id, request);
if (updated <= 0) {
throw new BusinessException("未找到可编辑的渠道拓展记录");
}
}
@Override
public Long createFollowUp(Long userId, String bizType, Long bizId, CreateExpansionFollowUpRequest request) {
String normalizedBizType = normalizeBizType(bizType);
ensureOwnedExpansion(userId, normalizedBizType, bizId);
int inserted = expansionMapper.insertExpansionFollowUp(normalizedBizType, bizId, userId, request);
if (inserted <= 0) {
throw new BusinessException("跟进记录新增失败");
}
return Long.valueOf(inserted);
}
private void attachSalesFollowUps(Long userId, List<SalesExpansionItemDTO> salesItems) {
List<Long> bizIds = salesItems.stream()
.map(SalesExpansionItemDTO::getId)
.filter(Objects::nonNull)
.toList();
if (bizIds.isEmpty()) {
return;
}
Map<Long, List<ExpansionFollowUpDTO>> grouped = expansionMapper.selectSalesFollowUps(userId, bizIds).stream()
.peek(this::fillFollowUpDisplayFields)
.collect(Collectors.groupingBy(ExpansionFollowUpDTO::getBizId));
for (SalesExpansionItemDTO item : salesItems) {
item.setFollowUps(grouped.getOrDefault(item.getId(), Collections.emptyList()));
}
}
private void attachChannelFollowUps(Long userId, List<ChannelExpansionItemDTO> channelItems) {
List<Long> bizIds = channelItems.stream()
.map(ChannelExpansionItemDTO::getId)
.filter(Objects::nonNull)
.toList();
if (bizIds.isEmpty()) {
return;
}
Map<Long, List<ExpansionFollowUpDTO>> grouped = expansionMapper.selectChannelFollowUps(userId, bizIds).stream()
.peek(this::fillFollowUpDisplayFields)
.collect(Collectors.groupingBy(ExpansionFollowUpDTO::getBizId));
for (ChannelExpansionItemDTO item : channelItems) {
item.setFollowUps(grouped.getOrDefault(item.getId(), Collections.emptyList()));
}
}
private void fillFollowUpDisplayFields(ExpansionFollowUpDTO followUp) {
if (followUp.getFollowUpTime() != null) {
followUp.setDate(followUp.getFollowUpTime().format(FOLLOW_UP_TIME_FORMATTER));
}
if (isBlank(followUp.getType())) {
followUp.setType("无");
}
if (isBlank(followUp.getContent())) {
followUp.setContent("无");
}
if (isBlank(followUp.getUser())) {
followUp.setUser("无");
}
}
private String normalizeKeyword(String keyword) {
if (keyword == null) {
return null;
}
String trimmed = keyword.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
private void fillSalesDefaults(CreateSalesExpansionRequest request) {
if (isBlank(request.getIntentLevel())) {
request.setIntentLevel("medium");
}
if (isBlank(request.getStage())) {
request.setStage("initial_contact");
}
if (request.getHasDesktopExp() == null) {
request.setHasDesktopExp(Boolean.FALSE);
}
if (request.getInProgress() == null) {
request.setInProgress(Boolean.TRUE);
}
if (isBlank(request.getEmploymentStatus())) {
request.setEmploymentStatus("active");
}
}
private void fillSalesDefaults(UpdateSalesExpansionRequest request) {
if (isBlank(request.getIntentLevel())) {
request.setIntentLevel("medium");
}
if (isBlank(request.getStage())) {
request.setStage("initial_contact");
}
if (request.getHasDesktopExp() == null) {
request.setHasDesktopExp(Boolean.FALSE);
}
if (request.getInProgress() == null) {
request.setInProgress(Boolean.TRUE);
}
if (isBlank(request.getEmploymentStatus())) {
request.setEmploymentStatus("active");
}
}
private void fillChannelDefaults(CreateChannelExpansionRequest request) {
if (isBlank(request.getStage())) {
request.setStage("initial_contact");
}
if (request.getLandedFlag() == null) {
request.setLandedFlag(Boolean.FALSE);
}
}
private void fillChannelDefaults(UpdateChannelExpansionRequest request) {
if (isBlank(request.getStage())) {
request.setStage("initial_contact");
}
if (request.getLandedFlag() == null) {
request.setLandedFlag(Boolean.FALSE);
}
}
private String normalizeBizType(String bizType) {
if ("sales".equalsIgnoreCase(bizType)) {
return "sales";
}
if ("channel".equalsIgnoreCase(bizType)) {
return "channel";
}
throw new BusinessException("不支持的拓展类型");
}
private void ensureOwnedExpansion(Long userId, String bizType, Long bizId) {
if (bizId == null || bizId <= 0) {
throw new BusinessException("拓展记录不存在");
}
int count = "sales".equals(bizType)
? expansionMapper.countOwnedSalesExpansion(userId, bizId)
: expansionMapper.countOwnedChannelExpansion(userId, bizId);
if (count <= 0) {
throw new BusinessException("无权操作该拓展记录");
}
}
}

View File

@ -0,0 +1,188 @@
package com.unis.crm.service.impl;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.unis.crm.common.BusinessException;
import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest;
import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO;
import com.unis.crm.dto.opportunity.OpportunityItemDTO;
import com.unis.crm.dto.opportunity.OpportunityOverviewDTO;
import com.unis.crm.mapper.OpportunityMapper;
import com.unis.crm.service.OpportunityService;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
@Service
public class OpportunityServiceImpl implements OpportunityService {
private final OpportunityMapper opportunityMapper;
public OpportunityServiceImpl(OpportunityMapper opportunityMapper) {
this.opportunityMapper = opportunityMapper;
}
@Override
public OpportunityOverviewDTO getOverview(Long userId, String keyword, String stage) {
String normalizedKeyword = normalizeKeyword(keyword);
String normalizedStage = normalizeStage(stage);
List<OpportunityItemDTO> items = opportunityMapper.selectOpportunities(userId, normalizedKeyword, normalizedStage);
attachFollowUps(userId, items);
return new OpportunityOverviewDTO(items);
}
@Override
public Long createOpportunity(Long userId, CreateOpportunityRequest request) {
fillDefaults(request);
Long customerId = opportunityMapper.selectOwnedCustomerIdByName(userId, request.getCustomerName().trim());
if (customerId == null) {
customerId = IdWorker.getId();
opportunityMapper.insertCustomer(customerId, userId, request.getCustomerName().trim(), request.getSource());
}
opportunityMapper.insertOpportunity(userId, customerId, request);
if (request.getId() == null) {
throw new BusinessException("商机新增失败");
}
return request.getId();
}
@Override
public Long updateOpportunity(Long userId, Long opportunityId, CreateOpportunityRequest request) {
if (opportunityId == null || opportunityId <= 0) {
throw new BusinessException("商机不存在");
}
if (opportunityMapper.countOwnedOpportunity(userId, opportunityId) <= 0) {
throw new BusinessException("无权编辑该商机");
}
fillDefaults(request);
Long customerId = opportunityMapper.selectOwnedCustomerIdByName(userId, request.getCustomerName().trim());
if (customerId == null) {
customerId = IdWorker.getId();
opportunityMapper.insertCustomer(customerId, userId, request.getCustomerName().trim(), request.getSource());
}
int updated = opportunityMapper.updateOpportunity(userId, opportunityId, customerId, request);
if (updated <= 0) {
throw new BusinessException("商机更新失败");
}
return opportunityId;
}
@Override
public Long createFollowUp(Long userId, Long opportunityId, CreateOpportunityFollowUpRequest request) {
if (opportunityId == null || opportunityId <= 0) {
throw new BusinessException("商机不存在");
}
if (opportunityMapper.countOwnedOpportunity(userId, opportunityId) <= 0) {
throw new BusinessException("无权操作该商机");
}
int inserted = opportunityMapper.insertOpportunityFollowUp(userId, opportunityId, request);
if (inserted <= 0) {
throw new BusinessException("商机跟进新增失败");
}
return Long.valueOf(inserted);
}
private void attachFollowUps(Long userId, List<OpportunityItemDTO> items) {
List<Long> opportunityIds = items.stream()
.map(OpportunityItemDTO::getId)
.filter(Objects::nonNull)
.toList();
if (opportunityIds.isEmpty()) {
return;
}
Map<Long, List<OpportunityFollowUpDTO>> grouped = opportunityMapper
.selectOpportunityFollowUps(userId, opportunityIds)
.stream()
.peek(this::fillFollowUpDisplayFields)
.collect(Collectors.groupingBy(OpportunityFollowUpDTO::getOpportunityId));
for (OpportunityItemDTO item : items) {
item.setFollowUps(grouped.getOrDefault(item.getId(), Collections.emptyList()));
}
}
private void fillFollowUpDisplayFields(OpportunityFollowUpDTO followUp) {
if (isBlank(followUp.getType())) {
followUp.setType("无");
}
if (isBlank(followUp.getContent())) {
followUp.setContent("无");
}
if (isBlank(followUp.getUser())) {
followUp.setUser("无");
}
if (!isBlank(followUp.getDate())) {
followUp.setDate(followUp.getDate());
}
}
private void fillDefaults(CreateOpportunityRequest request) {
request.setCustomerName(request.getCustomerName().trim());
request.setOpportunityName(request.getOpportunityName().trim());
if (request.getExpectedCloseDate() == null) {
throw new BusinessException("预计结单日期不能为空");
}
if (isBlank(request.getStage())) {
request.setStage("initial_contact");
} else {
request.setStage(toStageCode(request.getStage()));
}
if (isBlank(request.getOpportunityType())) {
request.setOpportunityType("新建");
}
if (isBlank(request.getProductType())) {
request.setProductType("VDI云桌面");
}
if (isBlank(request.getSource())) {
request.setSource("主动开发");
}
if (request.getPushedToOms() == null) {
request.setPushedToOms(Boolean.FALSE);
}
if (request.getConfidencePct() == null) {
request.setConfidencePct(50);
}
}
private String normalizeKeyword(String keyword) {
if (keyword == null) {
return null;
}
String trimmed = keyword.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private String normalizeStage(String stage) {
if (stage == null) {
return null;
}
String trimmed = stage.trim();
if (trimmed.isEmpty() || "全部".equals(trimmed)) {
return null;
}
return toStageCode(trimmed);
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
private String toStageCode(String value) {
return switch (value) {
case "初步沟通", "initial_contact" -> "initial_contact";
case "方案交流", "solution_discussion" -> "solution_discussion";
case "招投标", "bidding" -> "bidding";
case "商务谈判", "business_negotiation" -> "business_negotiation";
case "已成交", "won" -> "won";
case "已放弃", "lost" -> "lost";
default -> throw new BusinessException("不支持的商机阶段");
};
}
}

View File

@ -0,0 +1,333 @@
package com.unis.crm.service.impl;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.unis.crm.common.BusinessException;
import com.unis.crm.dto.work.CreateWorkCheckInRequest;
import com.unis.crm.dto.work.CreateWorkDailyReportRequest;
import com.unis.crm.dto.work.WorkCheckInDTO;
import com.unis.crm.dto.work.WorkDailyReportDTO;
import com.unis.crm.dto.work.WorkHistoryItemDTO;
import com.unis.crm.dto.work.WorkOverviewDTO;
import com.unis.crm.mapper.WorkMapper;
import com.unis.crm.service.WorkService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class WorkServiceImpl implements WorkService {
private static final String NOMINATIM_BASE_URL = "https://nominatim.openstreetmap.org/reverse";
private final WorkMapper workMapper;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
public WorkServiceImpl(WorkMapper workMapper, ObjectMapper objectMapper) {
this.workMapper = workMapper;
this.objectMapper = objectMapper;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(8))
.build();
}
@Override
public WorkOverviewDTO getOverview(Long userId) {
requireUser(userId);
WorkCheckInDTO todayCheckIn = workMapper.selectTodayCheckIn(userId);
enrichCheckInLocation(todayCheckIn);
WorkDailyReportDTO todayReport = workMapper.selectTodayReport(userId);
String suggestedWorkContent = buildSuggestedWorkContent(workMapper.selectTodayWorkContentLines(userId));
List<WorkHistoryItemDTO> history = workMapper.selectHistory(userId);
return new WorkOverviewDTO(todayCheckIn, todayReport, suggestedWorkContent, history);
}
@Override
public Long saveCheckIn(Long userId, CreateWorkCheckInRequest request) {
requireUser(userId);
request.setLocationText(request.getLocationText().trim());
request.setRemark(normalizeOptionalText(request.getRemark()));
int affectedRows = workMapper.insertCheckIn(userId, request);
Long checkInId = workMapper.selectTodayCheckInId(userId);
if (affectedRows <= 0 || checkInId == null) {
throw new BusinessException("外勤打卡保存失败");
}
return checkInId;
}
@Override
public Long saveDailyReport(Long userId, CreateWorkDailyReportRequest request) {
requireUser(userId);
request.setWorkContent(request.getWorkContent().trim());
request.setTomorrowPlan(request.getTomorrowPlan().trim());
request.setSourceType(normalizeOptionalText(request.getSourceType()));
if (request.getSourceType() == null) {
request.setSourceType("manual");
}
Long reportId = workMapper.selectTodayReportId(userId);
int affectedRows;
if (reportId == null) {
affectedRows = workMapper.insertDailyReport(userId, request);
reportId = workMapper.selectTodayReportId(userId);
} else {
affectedRows = workMapper.updateDailyReport(reportId, request);
}
if (affectedRows <= 0 || reportId == null) {
throw new BusinessException("日报保存失败");
}
syncTomorrowPlanTodo(userId, reportId, request.getTomorrowPlan());
return reportId;
}
@Override
public String resolveLocationName(BigDecimal latitude, BigDecimal longitude) {
if (latitude == null || longitude == null) {
throw new BusinessException("定位坐标不能为空");
}
try {
String requestUrl = NOMINATIM_BASE_URL
+ "?format=jsonv2&addressdetails=1&namedetails=1&extratags=1&zoom=18"
+ "&lat=" + URLEncoder.encode(latitude.stripTrailingZeros().toPlainString(), StandardCharsets.UTF_8)
+ "&lon=" + URLEncoder.encode(longitude.stripTrailingZeros().toPlainString(), StandardCharsets.UTF_8)
+ "&accept-language=" + URLEncoder.encode("zh-CN,zh;q=0.9,en;q=0.8", StandardCharsets.UTF_8);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(requestUrl))
.header("Accept", "application/json")
.header("User-Agent", "unis-crm-backend/1.0 (workbench reverse geocoding)")
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new BusinessException("地点解析失败,请稍后重试");
}
JsonNode root = objectMapper.readTree(response.body());
JsonNode addressNode = root.path("address");
String orderedLocation = buildOrderedLocationName(addressNode);
if (orderedLocation != null) {
return orderedLocation;
}
String displayName = textValue(root, "display_name");
if (displayName != null) {
String normalizedDisplayName = normalizeDisplayName(displayName);
if (normalizedDisplayName != null) {
return normalizedDisplayName;
}
return displayName;
}
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException("地点解析失败,请检查网络后重试");
}
throw new BusinessException("未能解析出具体地点名称");
}
private void requireUser(Long userId) {
if (userId == null || userId <= 0) {
throw new BusinessException("未获取到当前登录用户");
}
}
private String buildSuggestedWorkContent(List<String> lines) {
if (lines == null || lines.isEmpty()) {
return "";
}
StringBuilder builder = new StringBuilder();
int index = 1;
for (String line : lines) {
String normalized = normalizeOptionalText(line);
if (normalized == null) {
continue;
}
if (builder.length() > 0) {
builder.append('\n');
}
builder.append(index).append(". ").append(normalized);
index++;
}
return builder.toString();
}
private String normalizeOptionalText(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private String textValue(JsonNode node, String fieldName) {
if (node == null || node.isMissingNode()) {
return null;
}
String value = normalizeOptionalText(node.path(fieldName).asText(null));
return value;
}
private String joinNonBlank(String... values) {
StringBuilder builder = new StringBuilder();
for (String value : values) {
String normalized = normalizeOptionalText(value);
if (normalized == null) {
continue;
}
if (builder.indexOf(normalized) >= 0) {
continue;
}
builder.append(normalized);
}
return builder.length() == 0 ? null : builder.toString();
}
private String buildOrderedLocationName(JsonNode addressNode) {
if (addressNode == null || addressNode.isMissingNode()) {
return null;
}
return joinNonBlank(
textValue(addressNode, "state"),
firstNonBlank(
textValue(addressNode, "province"),
textValue(addressNode, "region"),
textValue(addressNode, "locality")),
firstNonBlank(
textValue(addressNode, "city"),
textValue(addressNode, "municipality"),
textValue(addressNode, "town"),
textValue(addressNode, "county")),
firstNonBlank(
textValue(addressNode, "district"),
textValue(addressNode, "city_district"),
textValue(addressNode, "borough")),
firstNonBlank(
textValue(addressNode, "suburb"),
textValue(addressNode, "township"),
textValue(addressNode, "residential"),
textValue(addressNode, "commercial"),
textValue(addressNode, "industrial"),
textValue(addressNode, "retail"),
textValue(addressNode, "quarter"),
textValue(addressNode, "neighbourhood"),
textValue(addressNode, "village"),
textValue(addressNode, "hamlet")),
firstNonBlank(
textValue(addressNode, "city_block"),
textValue(addressNode, "residential"),
textValue(addressNode, "commercial"),
textValue(addressNode, "allotments")),
firstNonBlank(
textValue(addressNode, "road"),
textValue(addressNode, "street"),
textValue(addressNode, "pedestrian")),
joinNonBlank(
textValue(addressNode, "house_number"),
textValue(addressNode, "house_name")),
firstNonBlank(
textValue(addressNode, "building"),
textValue(addressNode, "amenity"),
textValue(addressNode, "office"),
textValue(addressNode, "shop"))
);
}
private String normalizeDisplayName(String displayName) {
String normalized = normalizeOptionalText(displayName);
if (normalized == null) {
return null;
}
String[] segments = normalized.split("\\s*,\\s*");
if (segments.length == 0) {
return normalized;
}
StringBuilder builder = new StringBuilder();
for (int index = segments.length - 1; index >= 0; index--) {
String segment = normalizeOptionalText(segments[index]);
if (segment == null
|| "中国".equals(segment)
|| segment.matches("\\d{6}")
|| builder.indexOf(segment) >= 0) {
continue;
}
builder.append(segment);
}
return builder.length() == 0 ? normalized : builder.toString();
}
private void enrichCheckInLocation(WorkCheckInDTO todayCheckIn) {
if (todayCheckIn == null || todayCheckIn.getLatitude() == null || todayCheckIn.getLongitude() == null) {
return;
}
if (!shouldRefreshLocationText(todayCheckIn.getLocationText())) {
return;
}
try {
todayCheckIn.setLocationText(resolveLocationName(todayCheckIn.getLatitude(), todayCheckIn.getLongitude()));
} catch (Exception ignored) {
// Keep the stored location text when reverse geocoding is unavailable.
}
}
private boolean shouldRefreshLocationText(String locationText) {
String normalized = normalizeOptionalText(locationText);
if (normalized == null) {
return true;
}
return normalized.contains("")
|| normalized.contains(",")
|| normalized.length() < 14;
}
private void syncTomorrowPlanTodo(Long userId, Long reportId, String tomorrowPlan) {
String todoTitle = buildTomorrowPlanTodoTitle(tomorrowPlan);
Long todoId = workMapper.selectTodoIdByBiz(userId, "report", reportId);
if (todoId == null) {
workMapper.insertTodo(IdWorker.getId(), userId, todoTitle, "report", reportId);
return;
}
workMapper.updateTodo(todoId, todoTitle);
}
private String buildTomorrowPlanTodoTitle(String tomorrowPlan) {
String normalized = normalizeOptionalText(tomorrowPlan);
if (normalized == null) {
return "明日工作计划";
}
String singleLine = normalized.replace("\r", "\n").replace("\n", " ");
String title = "明日计划:" + singleLine;
return title.length() > 200 ? title.substring(0, 200) : title;
}
private String firstNonBlank(String... values) {
for (String value : values) {
String normalized = normalizeOptionalText(value);
if (normalized != null) {
return normalized;
}
}
return null;
}
}

View File

@ -0,0 +1,56 @@
server:
port: 8080
spring:
application:
name: unis-crm-backend
datasource:
url: jdbc:postgresql://127.0.0.1:5432/nex_auth
username: postgres
password: 199628
driver-class-name: org.postgresql.Driver
data:
redis:
host: 127.0.0.1
port: 6379
password: 199628@tlw
database: 14
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.unis.crm.dto.dashboard
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
com.unis.crm: info
unisbase:
tenant:
enabled: false
web:
auth-endpoints-enabled: true
management-endpoints-enabled: true
security:
enabled: true
mode: embedded
jwt-secret: change-me-please-change-me-32bytes
auth-header: Authorization
token-prefix: "Bearer "
permit-all-urls:
- /actuator/health
internal-auth:
enabled: true
secret: change-me-internal-secret
header-name: X-Internal-Secret
app:
upload-path: /Users/kangwenjing/Downloads/crm/uploads
resource-prefix: /sys/api/static/
captcha:
ttl-seconds: 120
max-attempts: 5
token:
access-default-minutes: 30
refresh-default-days: 7

View File

@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unis.crm.mapper.DashboardMapper">
<select id="selectDefaultUserId" resultType="java.lang.Long">
select user_id
from sys_user
where status = 1
order by user_id asc
limit 1
</select>
<select id="selectUserWelcome" resultType="com.unis.crm.dto.dashboard.UserWelcomeDTO">
select
u.user_id as userId,
u.display_name as realName,
null as jobTitle,
null as deptName,
null as hireDate
from sys_user u
where u.user_id = #{userId}
and u.status = 1
limit 1
</select>
<select id="selectDashboardStats" resultType="com.unis.crm.dto.dashboard.DashboardStatDTO">
select '本月新增商机' as name,
count(1)::bigint as value,
'monthlyOpportunities' as metricKey
from crm_opportunity
where owner_user_id = #{userId}
and date_trunc('month', created_at) = date_trunc('month', now())
union all
select '跟进中客户' as name,
count(1)::bigint as value,
'followingCustomers' as metricKey
from crm_customer
where owner_user_id = #{userId}
and status = 'following'
union all
select '已成单项目' as name,
count(1)::bigint as value,
'wonProjects' as metricKey
from crm_opportunity
where owner_user_id = #{userId}
and stage = 'won'
union all
select '本月打卡天数' as name,
count(distinct checkin_date)::bigint as value,
'monthlyCheckins' as metricKey
from work_checkin
where user_id = #{userId}
and date_trunc('month', checkin_date::timestamp) = date_trunc('month', now())
</select>
<select id="selectPendingTodos" resultType="com.unis.crm.dto.dashboard.DashboardTodoDTO">
select
id,
title,
biz_type as bizType,
biz_id as bizId,
priority,
status,
due_date as dueDate,
created_at as createdAt
from work_todo
where user_id = #{userId}
and status = 'todo'
order by
case priority
when 'high' then 1
when 'medium' then 2
else 3
end,
coalesce(due_date, created_at) asc
limit 6
</select>
<select id="selectLatestActivities" resultType="com.unis.crm.dto.dashboard.DashboardActivityDTO">
with latest_report_comment as (
select distinct on (c.report_id)
c.report_id,
c.reviewer_user_id,
c.score,
c.comment_content,
c.reviewed_at
from work_daily_report_comment c
order by c.report_id, c.reviewed_at desc, c.id desc
),
activity_union as (
select
l.id,
l.biz_type as bizType,
l.biz_id as bizId,
l.action_type as actionType,
l.title,
l.content,
l.operator_user_id as operatorUserId,
l.created_at as createdAt
from sys_activity_log l
where l.operator_user_id = #{userId}
or l.operator_user_id is null
union all
select
(1000000000 + o.id) as id,
'opportunity' as bizType,
o.id as bizId,
'stage_update' as actionType,
'商机阶段更新' as title,
o.opportunity_name || ' 已推进至' ||
case o.stage
when 'initial_contact' then '初步沟通'
when 'solution_discussion' then '方案交流'
when 'bidding' then '招投标'
when 'business_negotiation' then '商务谈判'
when 'won' then '已成交'
when 'lost' then '已输单'
else o.stage
end || '阶段' as content,
o.owner_user_id as operatorUserId,
o.updated_at as createdAt
from crm_opportunity o
where o.owner_user_id = #{userId}
and o.updated_at > o.created_at
union all
select
(2000000000 + r.id) as id,
'report' as bizType,
r.id as bizId,
case
when r.status = 'reviewed' or lc.score is not null then 'report_reviewed'
else 'report_read'
end as actionType,
case
when r.status = 'reviewed' or lc.score is not null then '日报已点评'
else '日报已阅'
end as title,
case
when lc.score is not null then '主管对你' || to_char(r.report_date, 'MM-DD') || '的日报给出了 ' || lc.score || ' 分'
when r.status = 'reviewed' then '你的' || to_char(r.report_date, 'MM-DD') || '日报已完成主管点评'
else '你的' || to_char(r.report_date, 'MM-DD') || '日报已被查阅'
end as content,
coalesce(lc.reviewer_user_id, r.user_id) as operatorUserId,
coalesce(lc.reviewed_at, r.updated_at, r.created_at) as createdAt
from work_daily_report r
left join latest_report_comment lc on lc.report_id = r.id
where r.user_id = #{userId}
and r.status in ('read', 'reviewed')
union all
select
(3000000000 + c.id) as id,
'channel' as bizType,
c.id as bizId,
'channel_created' as actionType,
'新渠道录入' as title,
'成功录入 ' || c.channel_name || ' 渠道商信息' as content,
c.owner_user_id as operatorUserId,
c.created_at as createdAt
from crm_channel_expansion c
where c.owner_user_id = #{userId}
union all
select
(4000000000 + f.id) as id,
'opportunity_followup' as bizType,
f.opportunity_id as bizId,
'opportunity_followup' as actionType,
'商机跟进新增' as title,
o.opportunity_name || ' 新增了一条' || f.followup_type || '跟进记录' as content,
f.followup_user_id as operatorUserId,
f.followup_time as createdAt
from crm_opportunity_followup f
join crm_opportunity o on o.id = f.opportunity_id
where f.followup_user_id = #{userId}
)
select
a.id,
a.bizType,
a.bizId,
a.actionType,
a.title,
a.content,
a.operatorUserId,
u.display_name as operatorName,
a.createdAt
from activity_union a
left join sys_user u on u.user_id = a.operatorUserId
order by a.createdAt desc nulls last
limit 8
</select>
</mapper>

View File

@ -0,0 +1,279 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unis.crm.mapper.ExpansionMapper">
<select id="selectDepartments" resultType="com.unis.crm.dto.expansion.DepartmentOptionDTO">
select
id,
org_name as name
from sys_org
where status = 1
order by id asc
</select>
<select id="selectSalesExpansions" resultType="com.unis.crm.dto.expansion.SalesExpansionItemDTO">
select
s.id,
'sales' as type,
s.candidate_name as name,
coalesce(s.mobile, '无') as phone,
coalesce(s.email, '无') as email,
s.target_dept_id as targetDeptId,
'无' as dept,
coalesce(s.industry, '无') as industry,
coalesce(s.title, '无') as title,
s.intent_level as intentLevel,
case s.intent_level
when 'high' then '高'
when 'medium' then '中'
when 'low' then '低'
else '无'
end as intent,
s.stage as stageCode,
case s.stage
when 'initial_contact' then '初步沟通'
when 'solution_discussion' then '方案交流'
when 'bidding' then '招投标'
when 'business_negotiation' then '商务谈判'
when 'won' then '已成交'
when 'lost' then '已放弃'
else coalesce(s.stage, '无')
end as stage,
s.has_desktop_exp as hasExp,
s.in_progress as inProgress,
(s.employment_status = 'active') as active,
s.employment_status as employmentStatus,
coalesce(to_char(s.expected_join_date, 'YYYY-MM-DD'), '无') as expectedJoinDate,
coalesce(s.remark, '无') as notes
from crm_sales_expansion s
where s.owner_user_id = #{userId}
<if test="keyword != null and keyword != ''">
and (
s.candidate_name ilike concat('%', #{keyword}, '%')
or coalesce(s.industry, '') ilike concat('%', #{keyword}, '%')
)
</if>
order by s.updated_at desc, s.id desc
</select>
<select id="selectChannelExpansions" resultType="com.unis.crm.dto.expansion.ChannelExpansionItemDTO">
select
c.id,
'channel' as type,
c.channel_name as name,
coalesce(c.province, '无') as province,
coalesce(c.industry, '无') as industry,
coalesce(cast(c.annual_revenue as varchar), '') as annualRevenue,
case
when c.annual_revenue is null then '无'
when c.annual_revenue >= 10000 then trim(to_char(c.annual_revenue / 10000.0, 'FM999999990.##')) || '万'
else trim(to_char(c.annual_revenue, 'FM999999990.##'))
end as revenue,
coalesce(c.staff_size, 0) as size,
coalesce(c.contact_name, '无') as contact,
coalesce(c.contact_title, '无') as contactTitle,
coalesce(c.contact_mobile, '无') as phone,
c.stage as stageCode,
case c.stage
when 'initial_contact' then '初步接触'
when 'solution_discussion' then '方案交流'
when 'bidding' then '招投标'
when 'business_negotiation' then '合作洽谈'
when 'won' then '已合作'
when 'lost' then '已终止'
else coalesce(c.stage, '无')
end as stage,
c.landed_flag as landed,
coalesce(to_char(c.expected_sign_date, 'YYYY-MM-DD'), '无') as expectedSignDate,
coalesce(c.remark, '无') as notes
from crm_channel_expansion c
where c.owner_user_id = #{userId}
<if test="keyword != null and keyword != ''">
and (
c.channel_name ilike concat('%', #{keyword}, '%')
or coalesce(c.industry, '') ilike concat('%', #{keyword}, '%')
or coalesce(c.province, '') ilike concat('%', #{keyword}, '%')
)
</if>
order by c.updated_at desc, c.id desc
</select>
<select id="selectSalesFollowUps" resultType="com.unis.crm.dto.expansion.ExpansionFollowUpDTO">
select
f.id,
f.biz_id as bizId,
f.biz_type as bizType,
f.followup_time as followUpTime,
f.followup_type as type,
coalesce(f.content, '无') as content,
coalesce(u.display_name, '无') as user
from crm_expansion_followup f
join crm_sales_expansion s on s.id = f.biz_id and f.biz_type = 'sales'
left join sys_user u on u.user_id = f.followup_user_id
where s.owner_user_id = #{userId}
and f.biz_id in
<foreach collection="bizIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
order by f.followup_time desc, f.id desc
</select>
<select id="selectChannelFollowUps" resultType="com.unis.crm.dto.expansion.ExpansionFollowUpDTO">
select
f.id,
f.biz_id as bizId,
f.biz_type as bizType,
f.followup_time as followUpTime,
f.followup_type as type,
coalesce(f.content, '无') as content,
coalesce(u.display_name, '无') as user
from crm_expansion_followup f
join crm_channel_expansion c on c.id = f.biz_id and f.biz_type = 'channel'
left join sys_user u on u.user_id = f.followup_user_id
where c.owner_user_id = #{userId}
and f.biz_id in
<foreach collection="bizIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
order by f.followup_time desc, f.id desc
</select>
<insert id="insertSalesExpansion" useGeneratedKeys="true" keyProperty="request.id">
insert into crm_sales_expansion (
candidate_name,
mobile,
email,
target_dept_id,
industry,
title,
intent_level,
stage,
has_desktop_exp,
in_progress,
employment_status,
expected_join_date,
owner_user_id,
remark
) values (
#{request.candidateName},
#{request.mobile},
#{request.email},
#{request.targetDeptId},
#{request.industry},
#{request.title},
#{request.intentLevel},
#{request.stage},
#{request.hasDesktopExp},
#{request.inProgress},
#{request.employmentStatus},
#{request.expectedJoinDate},
#{userId},
#{request.remark}
)
</insert>
<insert id="insertChannelExpansion" useGeneratedKeys="true" keyProperty="request.id">
insert into crm_channel_expansion (
channel_name,
province,
industry,
annual_revenue,
staff_size,
contact_name,
contact_title,
contact_mobile,
stage,
landed_flag,
expected_sign_date,
owner_user_id,
remark
) values (
#{request.channelName},
#{request.province},
#{request.industry},
#{request.annualRevenue},
#{request.staffSize},
#{request.contactName},
#{request.contactTitle},
#{request.contactMobile},
#{request.stage},
#{request.landedFlag},
#{request.expectedSignDate},
#{userId},
#{request.remark}
)
</insert>
<update id="updateSalesExpansion">
update crm_sales_expansion
set candidate_name = #{request.candidateName},
mobile = #{request.mobile},
email = #{request.email},
target_dept_id = #{request.targetDeptId},
industry = #{request.industry},
title = #{request.title},
intent_level = #{request.intentLevel},
stage = #{request.stage},
has_desktop_exp = #{request.hasDesktopExp},
in_progress = #{request.inProgress},
employment_status = #{request.employmentStatus},
expected_join_date = #{request.expectedJoinDate},
remark = #{request.remark}
where id = #{id}
and owner_user_id = #{userId}
</update>
<update id="updateChannelExpansion">
update crm_channel_expansion
set channel_name = #{request.channelName},
province = #{request.province},
industry = #{request.industry},
annual_revenue = #{request.annualRevenue},
staff_size = #{request.staffSize},
contact_name = #{request.contactName},
contact_title = #{request.contactTitle},
contact_mobile = #{request.contactMobile},
stage = #{request.stage},
landed_flag = #{request.landedFlag},
expected_sign_date = #{request.expectedSignDate},
remark = #{request.remark}
where id = #{id}
and owner_user_id = #{userId}
</update>
<select id="countOwnedSalesExpansion" resultType="int">
select count(1)
from crm_sales_expansion
where id = #{id}
and owner_user_id = #{userId}
</select>
<select id="countOwnedChannelExpansion" resultType="int">
select count(1)
from crm_channel_expansion
where id = #{id}
and owner_user_id = #{userId}
</select>
<insert id="insertExpansionFollowUp">
insert into crm_expansion_followup (
biz_type,
biz_id,
followup_time,
followup_type,
content,
next_action,
followup_user_id
) values (
#{bizType},
#{bizId},
#{request.followUpTime},
#{request.followUpType},
#{request.content},
#{request.nextAction},
#{userId}
)
</insert>
</mapper>

View File

@ -0,0 +1,197 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unis.crm.mapper.OpportunityMapper">
<select id="selectOpportunities" resultType="com.unis.crm.dto.opportunity.OpportunityItemDTO">
select
o.id,
o.opportunity_code as code,
o.opportunity_name as name,
coalesce(c.customer_name, '未命名客户') as client,
coalesce(u.display_name, '当前用户') as owner,
o.amount,
to_char(o.expected_close_date, 'YYYY-MM-DD') as date,
o.confidence_pct as confidence,
case coalesce(o.stage, 'initial_contact')
when 'initial_contact' then '初步沟通'
when 'solution_discussion' then '方案交流'
when 'bidding' then '招投标'
when 'business_negotiation' then '商务谈判'
when 'won' then '已成交'
when 'lost' then '已放弃'
else coalesce(o.stage, '初步沟通')
end as stage,
coalesce(o.opportunity_type, '新建') as type,
coalesce(o.pushed_to_oms, false) as pushedToOms,
coalesce(o.product_type, 'VDI云桌面') as product,
coalesce(o.source, '主动开发') as source,
coalesce(o.description, '') as notes
from crm_opportunity o
left join crm_customer c on c.id = o.customer_id
left join sys_user u on u.user_id = o.owner_user_id
where o.owner_user_id = #{userId}
<if test="keyword != null and keyword != ''">
and (
o.opportunity_name ilike concat('%', #{keyword}, '%')
or o.opportunity_code ilike concat('%', #{keyword}, '%')
or coalesce(c.customer_name, '') ilike concat('%', #{keyword}, '%')
)
</if>
<if test="stage != null and stage != ''">
and o.stage = #{stage}
</if>
order by coalesce(o.updated_at, o.created_at) desc, o.id desc
</select>
<select id="selectOpportunityFollowUps" resultType="com.unis.crm.dto.opportunity.OpportunityFollowUpDTO">
select
f.id,
f.opportunity_id as opportunityId,
to_char(f.followup_time, 'YYYY-MM-DD HH24:MI') as date,
coalesce(f.followup_type, '无') as type,
coalesce(f.content, '无') as content,
coalesce(u.display_name, '无') as user
from crm_opportunity_followup f
join crm_opportunity o on o.id = f.opportunity_id
left join sys_user u on u.user_id = f.followup_user_id
where o.owner_user_id = #{userId}
and f.opportunity_id in
<foreach collection="opportunityIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
order by f.followup_time desc, f.id desc
</select>
<select id="selectOwnedCustomerIdByName" resultType="java.lang.Long">
select id
from crm_customer
where owner_user_id = #{userId}
and customer_name = #{customerName}
limit 1
</select>
<insert id="insertCustomer">
insert into crm_customer (
id,
customer_code,
customer_name,
owner_user_id,
source,
status,
created_at,
updated_at
) values (
#{id},
'CUS-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad((coalesce((select count(1) from crm_customer), 0) + 1)::text, 3, '0'),
#{customerName},
#{userId},
coalesce(#{source}, '主动开发'),
'following',
now(),
now()
)
</insert>
<insert id="insertOpportunity" useGeneratedKeys="true" keyProperty="request.id">
insert into crm_opportunity (
opportunity_code,
opportunity_name,
customer_id,
owner_user_id,
amount,
expected_close_date,
confidence_pct,
stage,
opportunity_type,
product_type,
source,
pushed_to_oms,
oms_push_time,
description,
status,
created_at,
updated_at
) values (
'OPP-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad((coalesce((select count(1) from crm_opportunity), 0) + 1)::text, 3, '0'),
#{request.opportunityName},
#{customerId},
#{userId},
#{request.amount},
#{request.expectedCloseDate},
#{request.confidencePct},
#{request.stage},
#{request.opportunityType},
#{request.productType},
#{request.source},
#{request.pushedToOms},
case when #{request.pushedToOms} then now() else null end,
#{request.description},
case
when #{request.stage} = 'won' then 'won'
when #{request.stage} = 'lost' then 'lost'
else 'active'
end,
now(),
now()
)
</insert>
<select id="countOwnedOpportunity" resultType="int">
select count(1)
from crm_opportunity
where id = #{id}
and owner_user_id = #{userId}
</select>
<update id="updateOpportunity">
update crm_opportunity
set opportunity_name = #{request.opportunityName},
customer_id = #{customerId},
amount = #{request.amount},
expected_close_date = #{request.expectedCloseDate},
confidence_pct = #{request.confidencePct},
stage = #{request.stage},
opportunity_type = #{request.opportunityType},
product_type = #{request.productType},
source = #{request.source},
pushed_to_oms = #{request.pushedToOms},
oms_push_time = case
when #{request.pushedToOms} then coalesce(oms_push_time, now())
else null
end,
description = #{request.description},
status = case
when #{request.stage} = 'won' then 'won'
when #{request.stage} = 'lost' then 'lost'
else 'active'
end,
updated_at = now()
where id = #{opportunityId}
and owner_user_id = #{userId}
</update>
<insert id="insertOpportunityFollowUp">
insert into crm_opportunity_followup (
opportunity_id,
followup_time,
followup_type,
content,
next_action,
followup_user_id,
created_at,
updated_at
) values (
#{opportunityId},
#{request.followUpTime},
#{request.followUpType},
#{request.content},
#{request.nextAction},
#{userId},
now(),
now()
)
</insert>
</mapper>

View File

@ -0,0 +1,374 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.unis.crm.mapper.WorkMapper">
<select id="selectTodayCheckIn" resultType="com.unis.crm.dto.work.WorkCheckInDTO">
select
id,
to_char(checkin_date, 'YYYY-MM-DD') as date,
to_char(checkin_time, 'HH24:MI') as time,
coalesce(location_text, '') as locationText,
coalesce(remark, '') as remark,
coalesce(status, 'normal') as status,
longitude,
latitude
from work_checkin
where user_id = #{userId}
and checkin_date = current_date
order by checkin_time desc nulls last, id desc
limit 1
</select>
<select id="selectTodayReport" resultType="com.unis.crm.dto.work.WorkDailyReportDTO">
select
r.id,
to_char(r.report_date, 'YYYY-MM-DD') as date,
to_char(r.submit_time, 'YYYY-MM-DD HH24:MI') as submitTime,
coalesce(r.work_content, '') as workContent,
coalesce(r.tomorrow_plan, '') as tomorrowPlan,
coalesce(r.source_type, 'manual') as sourceType,
coalesce(r.status, 'submitted') as status,
c.score,
c.comment_content as comment
from work_daily_report r
left join (
select distinct on (report_id)
report_id,
score,
comment_content
from work_daily_report_comment
order by report_id, reviewed_at desc nulls last, id desc
) c on c.report_id = r.id
where r.user_id = #{userId}
and r.report_date = current_date
order by r.submit_time desc nulls last, r.id desc
limit 1
</select>
<select id="selectTodayWorkContentLines" resultType="java.lang.String">
select line
from (
select
coalesce(s.created_at, now()) as action_time,
'新增销售拓展:' ||
coalesce(s.candidate_name, '未命名') ||
case
when s.title is not null and btrim(s.title) &lt;&gt; '' then ',岗位:' || s.title
else ''
end ||
case
when s.intent_level is not null and btrim(s.intent_level) &lt;&gt; '' then ',意向:' || s.intent_level
else ''
end as line
from crm_sales_expansion s
where s.owner_user_id = #{userId}
and s.created_at::date = current_date
union all
select
coalesce(c.created_at, now()) as action_time,
'新增渠道拓展:' ||
coalesce(c.channel_name, '未命名渠道') ||
case
when c.province is not null and btrim(c.province) &lt;&gt; '' then ',地区:' || c.province
else ''
end ||
case
when c.industry is not null and btrim(c.industry) &lt;&gt; '' then ',行业:' || c.industry
else ''
end as line
from crm_channel_expansion c
where c.owner_user_id = #{userId}
and c.created_at::date = current_date
union all
select
coalesce(o.created_at, now()) as action_time,
'新增商机:' ||
coalesce(o.opportunity_name, '未命名商机') ||
case
when cust.customer_name is not null and btrim(cust.customer_name) &lt;&gt; '' then ',客户:' || cust.customer_name
else ''
end ||
case
when o.amount is not null then ',金额:¥' || trim(to_char(o.amount, 'FM9999999999990.00'))
else ''
end as line
from crm_opportunity o
left join crm_customer cust on cust.id = o.customer_id
where o.owner_user_id = #{userId}
and o.created_at::date = current_date
union all
select
coalesce(f.followup_time, now()) as action_time,
'销售拓展跟进:' ||
coalesce(s.candidate_name, '未命名') ||
case
when f.followup_type is not null and btrim(f.followup_type) &lt;&gt; '' then ',方式:' || f.followup_type
else ''
end ||
case
when f.content is not null and btrim(f.content) &lt;&gt; '' then ',内容:' || f.content
else ''
end as line
from crm_expansion_followup f
join crm_sales_expansion s on s.id = f.biz_id and f.biz_type = 'sales'
where f.followup_user_id = #{userId}
and f.followup_time::date = current_date
union all
select
coalesce(f.followup_time, now()) as action_time,
'渠道拓展跟进:' ||
coalesce(c.channel_name, '未命名渠道') ||
case
when f.followup_type is not null and btrim(f.followup_type) &lt;&gt; '' then ',方式:' || f.followup_type
else ''
end ||
case
when f.content is not null and btrim(f.content) &lt;&gt; '' then ',内容:' || f.content
else ''
end as line
from crm_expansion_followup f
join crm_channel_expansion c on c.id = f.biz_id and f.biz_type = 'channel'
where f.followup_user_id = #{userId}
and f.followup_time::date = current_date
union all
select
coalesce(f.followup_time, now()) as action_time,
'商机跟进:' ||
coalesce(o.opportunity_name, '未命名商机') ||
case
when f.followup_type is not null and btrim(f.followup_type) &lt;&gt; '' then ',方式:' || f.followup_type
else ''
end ||
case
when f.content is not null and btrim(f.content) &lt;&gt; '' then ',内容:' || f.content
else ''
end as line
from crm_opportunity_followup f
join crm_opportunity o on o.id = f.opportunity_id
where f.followup_user_id = #{userId}
and f.followup_time::date = current_date
) work_lines
order by action_time asc, line asc
</select>
<select id="selectHistory" resultType="com.unis.crm.dto.work.WorkHistoryItemDTO">
select
id,
type,
date,
time,
content,
status,
score,
comment
from (
select
c.id,
'外勤打卡' as type,
to_char(c.checkin_date, 'YYYY-MM-DD') as date,
to_char(c.checkin_time, 'HH24:MI') as time,
coalesce(c.location_text, '') ||
case
when c.remark is not null and btrim(c.remark) &lt;&gt; '' then E'\n备注' || c.remark
else ''
end as content,
case coalesce(c.status, 'normal')
when 'normal' then '正常'
when 'updated' then '已更新'
else coalesce(c.status, '正常')
end as status,
null::integer as score,
null::text as comment,
coalesce(c.checkin_date::timestamp + c.checkin_time::time, c.created_at) as sort_time
from work_checkin c
where c.user_id = #{userId}
union all
select
r.id,
'日报' as type,
to_char(r.report_date, 'YYYY-MM-DD') as date,
to_char(r.submit_time, 'HH24:MI') as time,
coalesce(r.work_content, '') ||
case
when r.tomorrow_plan is not null and btrim(r.tomorrow_plan) &lt;&gt; '' then E'\n明日计划' || r.tomorrow_plan
else ''
end as content,
case coalesce(rc.comment_content, '')
when '' then
case coalesce(r.status, 'submitted')
when 'submitted' then '已提交'
when 'reviewed' then '已点评'
else coalesce(r.status, '已提交')
end
else '已点评'
end as status,
rc.score,
rc.comment_content as comment,
coalesce(r.report_date::timestamp + r.submit_time::time, r.created_at) as sort_time
from work_daily_report r
left join (
select distinct on (report_id)
report_id,
score,
comment_content
from work_daily_report_comment
order by report_id, reviewed_at desc nulls last, id desc
) rc on rc.report_id = r.id
where r.user_id = #{userId}
) history
order by sort_time desc nulls last, id desc
</select>
<select id="selectTodayCheckInId" resultType="java.lang.Long">
select id
from work_checkin
where user_id = #{userId}
and checkin_date = current_date
order by checkin_time desc nulls last, id desc
limit 1
</select>
<insert id="insertCheckIn">
insert into work_checkin (
id,
user_id,
checkin_date,
checkin_time,
longitude,
latitude,
location_text,
remark,
status,
created_at,
updated_at
) values (
(select coalesce(max(id), 0) + 1 from work_checkin),
#{userId},
current_date,
now(),
#{request.longitude},
#{request.latitude},
#{request.locationText},
#{request.remark},
'normal',
now(),
now()
)
</insert>
<update id="updateCheckIn">
update work_checkin
set checkin_time = now(),
longitude = #{request.longitude},
latitude = #{request.latitude},
location_text = #{request.locationText},
remark = #{request.remark},
status = 'normal',
updated_at = now()
where id = #{checkInId}
</update>
<select id="selectTodayReportId" resultType="java.lang.Long">
select id
from work_daily_report
where user_id = #{userId}
and report_date = current_date
order by submit_time desc nulls last, id desc
limit 1
</select>
<insert id="insertDailyReport">
insert into work_daily_report (
user_id,
report_date,
work_content,
tomorrow_plan,
source_type,
submit_time,
status,
created_at,
updated_at
) values (
#{userId},
current_date,
#{request.workContent},
#{request.tomorrowPlan},
#{request.sourceType},
now(),
'submitted',
now(),
now()
)
</insert>
<update id="updateDailyReport">
update work_daily_report
set work_content = #{request.workContent},
tomorrow_plan = #{request.tomorrowPlan},
source_type = #{request.sourceType},
submit_time = now(),
status = 'submitted',
updated_at = now()
where id = #{reportId}
</update>
<select id="selectTodoIdByBiz" resultType="java.lang.Long">
select id
from work_todo
where user_id = #{userId}
and biz_type = #{bizType}
and biz_id = #{bizId}
limit 1
</select>
<insert id="insertTodo">
insert into work_todo (
id,
user_id,
title,
biz_type,
biz_id,
due_date,
status,
priority,
created_at,
updated_at
) values (
#{todoId},
#{userId},
#{title},
#{bizType},
#{bizId},
current_date::timestamp + interval '1 day' + time '09:00',
'todo',
'medium',
now(),
now()
)
</insert>
<update id="updateTodo">
update work_todo
set title = #{title},
due_date = current_date::timestamp + interval '1 day' + time '09:00',
status = 'todo',
priority = 'medium',
updated_at = now()
where id = #{todoId}
</update>
</mapper>

View File

@ -0,0 +1,56 @@
server:
port: 8080
spring:
application:
name: unis-crm-backend
datasource:
url: jdbc:postgresql://127.0.0.1:5432/nex_auth
username: postgres
password: 199628
driver-class-name: org.postgresql.Driver
data:
redis:
host: 127.0.0.1
port: 6379
password: 199628@tlw
database: 14
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.unis.crm.dto.dashboard
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
com.unis.crm: info
unisbase:
tenant:
enabled: false
web:
auth-endpoints-enabled: true
management-endpoints-enabled: true
security:
enabled: true
mode: embedded
jwt-secret: change-me-please-change-me-32bytes
auth-header: Authorization
token-prefix: "Bearer "
permit-all-urls:
- /actuator/health
internal-auth:
enabled: true
secret: change-me-internal-secret
header-name: X-Internal-Secret
app:
upload-path: /Users/kangwenjing/Downloads/crm/uploads
resource-prefix: /sys/api/static/
captcha:
ttl-seconds: 120
max-attempts: 5
token:
access-default-minutes: 30
refresh-default-days: 7

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