commit 60c416e553c57c03c6cc6eb14e050b816ea86ea9 Author: andy <594580820@qq.com> Date: Fri Jan 23 17:20:52 2026 +0800 初始化项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c190bff --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.idea/ +/tmpclaude-6f5d-cwd +/tmpclaude-006c-cwd +/tmpclaude-27a4-cwd +/tmpclaude-2246-cwd +/tmpclaude-a7d6-cwd +/tmpclaude-b78e-cwd diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4488232 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,124 @@ +- # CLAUDE.md + + 此文件为 Claude Code (claude.ai/code) 在此仓库中工作时提供指导。 + + ## 项目名称 + 经销商管理系统 + + ## 项目概述 + 经销商管理系统是一款面向企业渠道管理的CRM系统,核心解决“客户报备冲突”与“保护期归属”问题。系统通过**唯一有效报备机制**与**自动到期释放机制**,确保同一客户在同一时间只能被一个经销商跟进,并在保护期结束后自动回归可报备池,避免长期占坑,提升渠道效率与公平性。 + + ## 技术栈 + - 前端:Vue 3 + TypeScript + Vite + Element Plus + - 后端:Spring Boot 2.7 + MyBatis + MySQL 8 + - 架构模式:前后端分离,MVC 分层 + - 构建工具:Maven(后端)、pnpm(前端) + - 代码规范:ESLint + Prettier(前端)、SpotBugs + Checkstyle(后端) + + ## 项目结构 + ``` + by-crm/ + ├── backend/ # Spring Boot 工程 + │ ├── src/main/com/bycrm/ + │ │ ├── controller/ # Web 层,RESTful API + │ │ ├── service/ # 业务层 + │ │ ├── mapper/ # MyBatis DAO + │ │ ├── entity/ # PO / DTO / VO + │ │ ├── config/ # 跨域、MyBatis、Swagger 等配置 + │ │ └── ByCrmApplication.java + │ ├── src/main/resources/ + │ │ ├── mapper/*.xml # SQL 映射 + │ │ └── application.yml # 数据源、MyBatis、JWT、有效期等配置 + │ └── pom.xml + ├── frontend/ # Vue 3 工程 + │ ├── src/ + │ │ ├── api/ # 接口封装(axios) + │ │ ├── views/ # 页面级组件 + │ │ ├── components/ # 通用组件 + │ │ ├── router/ # Vue Router + │ │ ├── stores/ # Pinia 状态 + │ │ ├── types/ # TypeScript 类型 + │ │ └── utils/ # 权限、请求拦截、日期格式化 + │ ├── vite.config.ts + │ └── package.json + ├── sql/ + │ └── init.sql # 客户表、报备表、经销商表、字典表 + └── docs/ + └── api.md # Swagger 导出文档 + ``` + + ## 开发命令 + ### 后端 + ```bash + cd backend + mvn spring-boot:run # 本地启动(端口 8080) + mvn test # 单元测试 + mvn spotbugs:check # 静态检查 + ``` + + ### 前端 + ```bash + cd frontend + pnpm install + pnpm dev # 本地启动(端口 5173) + pnpm build # 生成 dist + pnpm lint # ESLint 检查 + ``` + + ## 核心业务流程(供编码时参考) + 1. 客户唯一性校验:新建客户时先模糊匹配名称,命中则禁止重复创建。 + 2. 报备申请:经销商选择客户 → 填写简要说明 → 提交。 + 3. 防撞单:提交瞬间校验该客户是否已存在“有效”报备,存在则直接提示冲突。 + 4. 审核:管理员在后台查看待审核列表 → 通过(生成保护期)/ 驳回(填写原因)。 + 5. 保护期:通过后自动计算到期日(默认 90 天,可配置),到期凌晨定时任务将状态置为“已失效”,客户重新变为“可报备”。 + 6. 数据可见性:经销商仅看“自己的客户 + 自己的报备”;管理员可看全部。 + + ## 关键配置项 + - `application.yml` 中的 `crm.report.ttl-days` 控制保护期天数。 + - `crm.report.allow-overlap` 仅用于测试开关,生产必须保持 false。 + - Vue 全局拦截器统一注入 JWT(登录后返回),权限粒度到按钮级。 + + ## 数据库命名规范 + - 表名:全小写,下划线分割,如 `customer`, `report`, `dealer`,以 `crm_` 开头。 + - 字段:全小写,下划线,主键 `id`,不需要外键,外键由程序控制,时间 `created_at`, `updated_at`。 + - 字典:使用 `tinyint` 并配套枚举类,如 `customer_status` 0-可报备 1-保护中。 + + ## 后续迭代方向 + - 微信小程序端提交报备 + - 客户公海池与主动分配 + - 报备延期申请流程 + - 报表:经销商活跃度、客户转化率 + + ## API 接口说明 + ### 认证接口 + - POST `/api/auth/login` - 用户登录 + - GET `/api/auth/user/info` - 获取当前用户信息 + - POST `/api/auth/logout` - 用户退出 + + ### 客户接口 + - GET `/api/customer/page` - 分页查询客户 + - GET `/api/customer/{id}` - 获取客户详情 + - POST `/api/customer` - 创建客户 + - PUT `/api/customer/{id}` - 更新客户 + - DELETE `/api/customer/{id}` - 删除客户 + - GET `/api/customer/search` - 搜索客户(用于客户唯一性校验) + + ### 报备接口 + - GET `/api/report/page` - 分页查询报备 + - GET `/api/report/{id}` - 获取报备详情 + - POST `/api/report` - 创建报备 + - PUT `/api/report/{id}/audit` - 审核报备 + - DELETE `/api/report/{id}` - 撤回报备 + + ### 经销商接口(仅管理员) + - GET `/api/dealer/list` - 查询所有经销商 + - GET `/api/dealer/{id}` - 获取经销商详情 + - POST `/api/dealer` - 创建经销商 + - PUT `/api/dealer/{id}` - 更新经销商 + - DELETE `/api/dealer/{id}` - 删除经销商 + + ## 默认测试账号 + - 管理员:`admin` / `admin123` + - 经销商用户1:`user001` / `admin123` + - 经销商用户2:`user002` / `admin123` + - 经销商用户3:`user003` / `admin123` diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..db54d52 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + com.bycrm + by-crm-backend + 1.0.0 + BY-CRM Backend + 经销商管理系统后端服务 + + + 1.8 + 2.3.1 + 1.2.20 + 0.11.5 + 3.0.0 + 3.12.0 + 5.8.23 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis-spring-boot.version} + + + + + mysql + mysql-connector-java + 8.0.33 + + + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + + + io.jsonwebtoken + jjwt-api + ${jwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jwt.version} + runtime + + + + + io.springfox + springfox-boot-starter + ${swagger.version} + + + + + org.apache.commons + commons-lang3 + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + com.github.spotbugs + spotbugs-annotations + 4.7.3 + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.7.3.6 + + + com.github.spotbugs + spotbugs + 4.7.3 + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + + checkstyle.xml + UTF-8 + true + false + + + + + diff --git a/backend/src/main/com/bycrm/ByCrmApplication.java b/backend/src/main/com/bycrm/ByCrmApplication.java new file mode 100644 index 0000000..cab7427 --- /dev/null +++ b/backend/src/main/com/bycrm/ByCrmApplication.java @@ -0,0 +1,21 @@ +package com.bycrm; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 经销商管理系统主启动类 + */ +@SpringBootApplication +@EnableScheduling +public class ByCrmApplication { + + public static void main(String[] args) { + SpringApplication.run(ByCrmApplication.class, args); + System.out.println("\n========================================"); + System.out.println("经销商管理系统启动成功!"); + System.out.println("Swagger文档地址: http://localhost:8080/api/swagger-ui/index.html"); + System.out.println("========================================\n"); + } +} diff --git a/backend/src/main/com/bycrm/annotations/AuthRequired.java b/backend/src/main/com/bycrm/annotations/AuthRequired.java new file mode 100644 index 0000000..b1674f5 --- /dev/null +++ b/backend/src/main/com/bycrm/annotations/AuthRequired.java @@ -0,0 +1,14 @@ +package com.bycrm.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 需要认证的注解 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthRequired { +} diff --git a/backend/src/main/com/bycrm/common/Constants.java b/backend/src/main/com/bycrm/common/Constants.java new file mode 100644 index 0000000..468b0b8 --- /dev/null +++ b/backend/src/main/com/bycrm/common/Constants.java @@ -0,0 +1,92 @@ +package com.bycrm.common; + +/** + * 系统常量 + */ +public class Constants { + + /** + * JWT Token 前缀 + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * JWT Token Header + */ + public static final String TOKEN_HEADER = "Authorization"; + + /** + * 用户信息在 Redis 中的前缀 + */ + public static final String USER_TOKEN_PREFIX = "user:token:"; + + /** + * 客户状态 - 可报备 + */ + public static final Integer CUSTOMER_STATUS_AVAILABLE = 0; + + /** + * 客户状态 - 保护中 + */ + public static final Integer CUSTOMER_STATUS_PROTECTED = 1; + + /** + * 客户状态 - 已失效 + */ + public static final Integer CUSTOMER_STATUS_EXPIRED = 2; + + /** + * 报备状态 - 待审核 + */ + public static final Integer REPORT_STATUS_PENDING = 0; + + /** + * 报备状态 - 已通过 + */ + public static final Integer REPORT_STATUS_APPROVED = 1; + + /** + * 报备状态 - 已驳回 + */ + public static final Integer REPORT_STATUS_REJECTED = 2; + + /** + * 报备状态 - 已失效 + */ + public static final Integer REPORT_STATUS_EXPIRED = 3; + + /** + * 用户角色 - 管理员 + */ + public static final Integer USER_ROLE_ADMIN = 0; + + /** + * 用户角色 - 经销商用户 + */ + public static final Integer USER_ROLE_DEALER = 1; + + /** + * 用户状态 - 禁用 + */ + public static final Integer USER_STATUS_DISABLED = 0; + + /** + * 用户状态 - 启用 + */ + public static final Integer USER_STATUS_ENABLED = 1; + + /** + * 经销商状态 - 禁用 + */ + public static final Integer DEALER_STATUS_DISABLED = 0; + + /** + * 经销商状态 - 启用 + */ + public static final Integer DEALER_STATUS_ENABLED = 1; + + /** + * 默认保护期天数 + */ + public static final Integer DEFAULT_PROTECT_DAYS = 90; +} diff --git a/backend/src/main/com/bycrm/common/PageResult.java b/backend/src/main/com/bycrm/common/PageResult.java new file mode 100644 index 0000000..19c7498 --- /dev/null +++ b/backend/src/main/com/bycrm/common/PageResult.java @@ -0,0 +1,55 @@ +package com.bycrm.common; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 分页结果 + */ +@Data +public class PageResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 总记录数 + */ + private Long total; + + /** + * 当前页数据 + */ + private List records; + + /** + * 当前页 + */ + private Long current; + + /** + * 每页大小 + */ + private Long size; + + /** + * 总页数 + */ + private Long pages; + + public PageResult() { + } + + public PageResult(Long total, List records, Long current, Long size) { + this.total = total; + this.records = records; + this.current = current; + this.size = size; + this.pages = (total + size - 1) / size; + } + + public static PageResult of(Long total, List records, Long current, Long size) { + return new PageResult<>(total, records, current, size); + } +} diff --git a/backend/src/main/com/bycrm/common/Result.java b/backend/src/main/com/bycrm/common/Result.java new file mode 100644 index 0000000..caed6d8 --- /dev/null +++ b/backend/src/main/com/bycrm/common/Result.java @@ -0,0 +1,94 @@ +package com.bycrm.common; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 统一响应结果 + */ +@Data +public class Result implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 状态码 + */ + private Integer code; + + /** + * 响应消息 + */ + private String message; + + /** + * 响应数据 + */ + private T data; + + /** + * 时间戳 + */ + private Long timestamp; + + public Result() { + this.timestamp = System.currentTimeMillis(); + } + + public Result(Integer code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + this.timestamp = System.currentTimeMillis(); + } + + /** + * 成功返回(无数据) + */ + public static Result success() { + return new Result<>(200, "操作成功", null); + } + + /** + * 成功返回(有数据) + */ + public static Result success(T data) { + return new Result<>(200, "操作成功", data); + } + + /** + * 成功返回(自定义消息) + */ + public static Result success(String message, T data) { + return new Result<>(200, message, data); + } + + /** + * 失败返回 + */ + public static Result error(String message) { + return new Result<>(500, message, null); + } + + /** + * 失败返回(自定义状态码) + */ + public static Result error(Integer code, String message) { + return new Result<>(code, message, null); + } + + /** + * 未授权返回 + */ + public static Result unauthorized(String message) { + return new Result<>(401, message, null); + } + + /** + * 禁止访问返回 + */ + public static Result forbidden(String message) { + return new Result<>(403, message, null); + } +} diff --git a/backend/src/main/com/bycrm/config/AuthInterceptor.java b/backend/src/main/com/bycrm/config/AuthInterceptor.java new file mode 100644 index 0000000..ede75c0 --- /dev/null +++ b/backend/src/main/com/bycrm/config/AuthInterceptor.java @@ -0,0 +1,54 @@ +package com.bycrm.config; + +import com.bycrm.common.Constants; +import com.bycrm.util.JwtUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 认证拦截器 + */ +@Slf4j +@Component +public class AuthInterceptor implements HandlerInterceptor { + + private final JwtUtil jwtUtil; + + public AuthInterceptor(JwtUtil jwtUtil) { + this.jwtUtil = jwtUtil; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 设置当前用户ID到请求属性中 + String token = getTokenFromRequest(request); + + if (token != null && jwtUtil.validateToken(token)) { + Long userId = jwtUtil.getUserIdFromToken(token); + request.setAttribute("currentUserId", userId); + request.setAttribute("currentUserRole", jwtUtil.getRoleFromToken(token)); + request.setAttribute("currentUserDealerId", jwtUtil.getDealerIdFromToken(token)); + return true; + } + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"未授权,请先登录\"}"); + return false; + } + + /** + * 从请求中获取 Token + */ + private String getTokenFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader(Constants.TOKEN_HEADER); + if (bearerToken != null && bearerToken.startsWith(Constants.TOKEN_PREFIX)) { + return bearerToken.substring(Constants.TOKEN_PREFIX.length()); + } + return null; + } +} diff --git a/backend/src/main/com/bycrm/config/CorsConfig.java b/backend/src/main/com/bycrm/config/CorsConfig.java new file mode 100644 index 0000000..4074e22 --- /dev/null +++ b/backend/src/main/com/bycrm/config/CorsConfig.java @@ -0,0 +1,22 @@ +package com.bycrm.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 跨域配置 + */ +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } +} diff --git a/backend/src/main/com/bycrm/config/MyBatisConfig.java b/backend/src/main/com/bycrm/config/MyBatisConfig.java new file mode 100644 index 0000000..06f3b4c --- /dev/null +++ b/backend/src/main/com/bycrm/config/MyBatisConfig.java @@ -0,0 +1,12 @@ +package com.bycrm.config; + +import org.springframework.context.annotation.Configuration; + +/** + * MyBatis 配置 + */ +@Configuration +public class MyBatisConfig { + + // MyBatis 配置主要通过 application.yml 配置 +} diff --git a/backend/src/main/com/bycrm/config/SwaggerConfig.java b/backend/src/main/com/bycrm/config/SwaggerConfig.java new file mode 100644 index 0000000..5826b12 --- /dev/null +++ b/backend/src/main/com/bycrm/config/SwaggerConfig.java @@ -0,0 +1,39 @@ +package com.bycrm.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.oas.annotations.EnableOpenApi; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +/** + * Swagger 配置 + */ +@Configuration +@EnableOpenApi +public class SwaggerConfig { + + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.OAS_30) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.bycrm.controller")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("经销商管理系统 API 文档") + .description("经销商管理系统 RESTful API 接口文档") + .contact(new Contact("BY CRM", "", "")) + .version("1.0.0") + .build(); + } +} diff --git a/backend/src/main/com/bycrm/config/WebMvcConfig.java b/backend/src/main/com/bycrm/config/WebMvcConfig.java new file mode 100644 index 0000000..bff7a4d --- /dev/null +++ b/backend/src/main/com/bycrm/config/WebMvcConfig.java @@ -0,0 +1,35 @@ +package com.bycrm.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; + +/** + * Web MVC 配置 + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final AuthInterceptor authInterceptor; + + public WebMvcConfig(AuthInterceptor authInterceptor) { + this.authInterceptor = authInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor) + .addPathPatterns("/**") + .excludePathPatterns(Arrays.asList( + "/auth/login", + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/webjars/**", + "/error", + "/druid/**" + )); + } +} diff --git a/backend/src/main/com/bycrm/controller/AuthController.java b/backend/src/main/com/bycrm/controller/AuthController.java new file mode 100644 index 0000000..a631921 --- /dev/null +++ b/backend/src/main/com/bycrm/controller/AuthController.java @@ -0,0 +1,79 @@ +package com.bycrm.controller; + +import com.bycrm.common.Result; +import com.bycrm.dto.LoginDTO; +import com.bycrm.service.UserService; +import com.bycrm.vo.UserInfoVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +/** + * 认证控制器 + */ +@Api(tags = "认证管理") +@RestController +@RequestMapping("/auth") +public class AuthController { + + private final UserService userService; + + public AuthController(UserService userService) { + this.userService = userService; + } + + /** + * 用户登录 + */ + @ApiOperation("用户登录") + @PostMapping("/login") + public Result login(@RequestBody LoginDTO loginDTO) { + String token = userService.login(loginDTO); + return Result.success(new LoginResponse(token)); + } + + /** + * 获取当前用户信息 + */ + @ApiOperation("获取当前用户信息") + @GetMapping("/user/info") + public Result getCurrentUser(HttpServletRequest request) { + String token = getTokenFromRequest(request); + UserInfoVO user = userService.getCurrentUser(token); + return Result.success(user); + } + + /** + * 用户退出 + */ + @ApiOperation("用户退出") + @PostMapping("/logout") + public Result logout() { + return Result.success(); + } + + /** + * 从请求中获取 Token + */ + private String getTokenFromRequest(HttpServletRequest request) { + String authorization = request.getHeader("Authorization"); + if (authorization != null && authorization.startsWith("Bearer ")) { + return authorization.substring(7); + } + return null; + } + + /** + * 登录响应 + */ + @lombok.Data + static class LoginResponse { + private String token; + + public LoginResponse(String token) { + this.token = token; + } + } +} diff --git a/backend/src/main/com/bycrm/controller/CustomerController.java b/backend/src/main/com/bycrm/controller/CustomerController.java new file mode 100644 index 0000000..d202453 --- /dev/null +++ b/backend/src/main/com/bycrm/controller/CustomerController.java @@ -0,0 +1,106 @@ +package com.bycrm.controller; + +import com.bycrm.common.PageResult; +import com.bycrm.common.Result; +import com.bycrm.dto.CustomerDTO; +import com.bycrm.dto.PageQuery; +import com.bycrm.entity.Customer; +import com.bycrm.service.CustomerService; +import com.bycrm.vo.CustomerVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * 客户控制器 + */ +@Api(tags = "客户管理") +@RestController +@RequestMapping("/customer") +public class CustomerController { + + private final CustomerService customerService; + + public CustomerController(CustomerService customerService) { + this.customerService = customerService; + } + + /** + * 分页查询客户 + */ + @ApiOperation("分页查询客户") + @GetMapping("/page") + public Result> getCustomerPage( + @ApiParam("当前页") @RequestParam(defaultValue = "1") Long current, + @ApiParam("每页大小") @RequestParam(defaultValue = "10") Long size, + @ApiParam("客户名称") @RequestParam(required = false) String name, + @ApiParam("所属行业") @RequestParam(required = false) String industry, + @ApiParam("状态") @RequestParam(required = false) Integer status) { + PageQuery query = new PageQuery(); + query.setCurrent(current); + query.setSize(size); + + PageResult result = customerService.getCustomerPage(query, name, industry, status); + return Result.success(result); + } + + /** + * 根据ID获取客户详情 + */ + @ApiOperation("根据ID获取客户详情") + @GetMapping("/{id}") + public Result getCustomerById(@ApiParam("客户ID") @PathVariable Long id) { + CustomerVO customer = customerService.getCustomerById(id); + return Result.success(customer); + } + + /** + * 创建客户 + */ + @ApiOperation("创建客户") + @PostMapping + public Result createCustomer(@RequestBody CustomerDTO customerDTO, HttpServletRequest request) { + Long currentUserId = (Long) request.getAttribute("currentUserId"); + Customer customer = customerService.createCustomer(customerDTO, currentUserId); + return Result.success(customer); + } + + /** + * 更新客户 + */ + @ApiOperation("更新客户") + @PutMapping("/{id}") + public Result updateCustomer( + @ApiParam("客户ID") @PathVariable Long id, + @RequestBody CustomerDTO customerDTO, + HttpServletRequest request) { + Long currentUserId = (Long) request.getAttribute("currentUserId"); + customerService.updateCustomer(id, customerDTO, currentUserId); + return Result.success(); + } + + /** + * 删除客户 + */ + @ApiOperation("删除客户") + @DeleteMapping("/{id}") + public Result deleteCustomer(@ApiParam("客户ID") @PathVariable Long id, HttpServletRequest request) { + Long currentUserId = (Long) request.getAttribute("currentUserId"); + customerService.deleteCustomer(id, currentUserId); + return Result.success(); + } + + /** + * 根据名称搜索客户(用于客户唯一性校验) + */ + @ApiOperation("根据名称搜索客户") + @GetMapping("/search") + public Result> searchByName(@ApiParam("客户名称") @RequestParam String name) { + List customers = customerService.searchByName(name); + return Result.success(customers); + } +} diff --git a/backend/src/main/com/bycrm/controller/DealerController.java b/backend/src/main/com/bycrm/controller/DealerController.java new file mode 100644 index 0000000..38a6618 --- /dev/null +++ b/backend/src/main/com/bycrm/controller/DealerController.java @@ -0,0 +1,82 @@ +package com.bycrm.controller; + +import com.bycrm.common.Result; +import com.bycrm.dto.DealerDTO; +import com.bycrm.entity.Dealer; +import com.bycrm.service.DealerService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 经销商控制器 + */ +@Api(tags = "经销商管理") +@RestController +@RequestMapping("/dealer") +public class DealerController { + + private final DealerService dealerService; + + public DealerController(DealerService dealerService) { + this.dealerService = dealerService; + } + + /** + * 查询所有经销商 + */ + @ApiOperation("查询所有经销商") + @GetMapping("/list") + public Result> getDealerList( + @ApiParam("经销商名称") @RequestParam(required = false) String name, + @ApiParam("经销商编码") @RequestParam(required = false) String code, + @ApiParam("状态") @RequestParam(required = false) Integer status) { + List dealers = dealerService.getDealerList(name, code, status); + return Result.success(dealers); + } + + /** + * 根据ID获取经销商 + */ + @ApiOperation("根据ID获取经销商") + @GetMapping("/{id}") + public Result getDealerById(@ApiParam("经销商ID") @PathVariable Long id) { + Dealer dealer = dealerService.getDealerById(id); + return Result.success(dealer); + } + + /** + * 创建经销商 + */ + @ApiOperation("创建经销商") + @PostMapping + public Result createDealer(@RequestBody DealerDTO dealerDTO) { + dealerService.createDealer(dealerDTO); + return Result.success(); + } + + /** + * 更新经销商 + */ + @ApiOperation("更新经销商") + @PutMapping("/{id}") + public Result updateDealer( + @ApiParam("经销商ID") @PathVariable Long id, + @RequestBody DealerDTO dealerDTO) { + dealerService.updateDealer(id, dealerDTO); + return Result.success(); + } + + /** + * 删除经销商 + */ + @ApiOperation("删除经销商") + @DeleteMapping("/{id}") + public Result deleteDealer(@ApiParam("经销商ID") @PathVariable Long id) { + dealerService.deleteDealer(id); + return Result.success(); + } +} diff --git a/backend/src/main/com/bycrm/controller/ReportController.java b/backend/src/main/com/bycrm/controller/ReportController.java new file mode 100644 index 0000000..9d9a55f --- /dev/null +++ b/backend/src/main/com/bycrm/controller/ReportController.java @@ -0,0 +1,99 @@ +package com.bycrm.controller; + +import com.bycrm.common.PageResult; +import com.bycrm.common.Result; +import com.bycrm.dto.PageQuery; +import com.bycrm.dto.ReportAuditDTO; +import com.bycrm.dto.ReportDTO; +import com.bycrm.service.ReportService; +import com.bycrm.vo.ReportVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +/** + * 报备控制器 + */ +@Api(tags = "报备管理") +@RestController +@RequestMapping("/report") +public class ReportController { + + private final ReportService reportService; + + public ReportController(ReportService reportService) { + this.reportService = reportService; + } + + /** + * 分页查询报备 + */ + @ApiOperation("分页查询报备") + @GetMapping("/page") + public Result> getReportPage( + @ApiParam("当前页") @RequestParam(defaultValue = "1") Long current, + @ApiParam("每页大小") @RequestParam(defaultValue = "10") Long size, + @ApiParam("经销商ID") @RequestParam(required = false) Long dealerId, + @ApiParam("经销商名称") @RequestParam(required = false) String dealerName, + @ApiParam("客户名称") @RequestParam(required = false) String customerName, + @ApiParam("状态") @RequestParam(required = false) Integer status, + HttpServletRequest request) { + Long currentUserId = (Long) request.getAttribute("currentUserId"); + + PageQuery query = new PageQuery(); + query.setCurrent(current); + query.setSize(size); + + PageResult result = reportService.getReportPage(query, dealerId, dealerName, customerName, status, currentUserId); + return Result.success(result); + } + + /** + * 根据ID获取报备详情 + */ + @ApiOperation("根据ID获取报备详情") + @GetMapping("/{id}") + public Result getReportById(@ApiParam("报备ID") @PathVariable Long id) { + ReportVO report = reportService.getReportById(id); + return Result.success(report); + } + + /** + * 创建报备 + */ + @ApiOperation("创建报备") + @PostMapping + public Result createReport(@RequestBody ReportDTO reportDTO, HttpServletRequest request) { + Long currentUserId = (Long) request.getAttribute("currentUserId"); + reportService.createReport(reportDTO, currentUserId); + return Result.success(); + } + + /** + * 审核报备 + */ + @ApiOperation("审核报备") + @PutMapping("/{id}/audit") + public Result auditReport( + @ApiParam("报备ID") @PathVariable Long id, + @RequestBody ReportAuditDTO auditDTO, + HttpServletRequest request) { + Long currentUserId = (Long) request.getAttribute("currentUserId"); + reportService.auditReport(id, auditDTO, currentUserId); + return Result.success(); + } + + /** + * 撤回报备 + */ + @ApiOperation("撤回报备") + @DeleteMapping("/{id}") + public Result withdrawReport(@ApiParam("报备ID") @PathVariable Long id, HttpServletRequest request) { + Long currentUserId = (Long) request.getAttribute("currentUserId"); + reportService.withdrawReport(id, currentUserId); + return Result.success(); + } +} diff --git a/backend/src/main/com/bycrm/dto/CustomerDTO.java b/backend/src/main/com/bycrm/dto/CustomerDTO.java new file mode 100644 index 0000000..d186801 --- /dev/null +++ b/backend/src/main/com/bycrm/dto/CustomerDTO.java @@ -0,0 +1,36 @@ +package com.bycrm.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * 客户请求 DTO + */ +@Data +public class CustomerDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 客户名称 + */ + @NotBlank(message = "客户名称不能为空") + private String name; + + /** + * 联系电话 + */ + private String phone; + + /** + * 地址 + */ + private String address; + + /** + * 所属行业 + */ + private String industry; +} diff --git a/backend/src/main/com/bycrm/dto/DealerDTO.java b/backend/src/main/com/bycrm/dto/DealerDTO.java new file mode 100644 index 0000000..66fb199 --- /dev/null +++ b/backend/src/main/com/bycrm/dto/DealerDTO.java @@ -0,0 +1,51 @@ +package com.bycrm.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * 经销商请求 DTO + */ +@Data +public class DealerDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 经销商名称 + */ + @NotBlank(message = "经销商名称不能为空") + private String name; + + /** + * 经销商编码 + */ + @NotBlank(message = "经销商编码不能为空") + private String code; + + /** + * 联系人 + */ + @NotBlank(message = "联系人不能为空") + private String contactPerson; + + /** + * 联系电话 + */ + @NotBlank(message = "联系电话不能为空") + private String contactPhone; + + /** + * 邮箱 + */ + private String email; + + /** + * 状态 + */ + @NotNull(message = "状态不能为空") + private Integer status; +} diff --git a/backend/src/main/com/bycrm/dto/LoginDTO.java b/backend/src/main/com/bycrm/dto/LoginDTO.java new file mode 100644 index 0000000..c42d459 --- /dev/null +++ b/backend/src/main/com/bycrm/dto/LoginDTO.java @@ -0,0 +1,27 @@ +package com.bycrm.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * 登录请求 DTO + */ +@Data +public class LoginDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户名 + */ + @NotBlank(message = "用户名不能为空") + private String username; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + private String password; +} diff --git a/backend/src/main/com/bycrm/dto/PageQuery.java b/backend/src/main/com/bycrm/dto/PageQuery.java new file mode 100644 index 0000000..fc879a1 --- /dev/null +++ b/backend/src/main/com/bycrm/dto/PageQuery.java @@ -0,0 +1,34 @@ +package com.bycrm.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 分页查询基类 + */ +@Data +public class PageQuery implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 当前页 + */ + private Long current = 1L; + + /** + * 每页大小 + */ + private Long size = 10L; + + /** + * 排序字段 + */ + private String orderBy; + + /** + * 排序方式:asc/desc + */ + private String orderDirection = "desc"; +} diff --git a/backend/src/main/com/bycrm/dto/ReportAuditDTO.java b/backend/src/main/com/bycrm/dto/ReportAuditDTO.java new file mode 100644 index 0000000..f30cc34 --- /dev/null +++ b/backend/src/main/com/bycrm/dto/ReportAuditDTO.java @@ -0,0 +1,32 @@ +package com.bycrm.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * 报备审核 DTO + */ +@Data +public class ReportAuditDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 报备ID + */ + @NotNull(message = "报备ID不能为空") + private Long reportId; + + /** + * 是否通过 + */ + @NotNull(message = "审核结果不能为空") + private Boolean approved; + + /** + * 驳回原因 + */ + private String rejectReason; +} diff --git a/backend/src/main/com/bycrm/dto/ReportDTO.java b/backend/src/main/com/bycrm/dto/ReportDTO.java new file mode 100644 index 0000000..776337e --- /dev/null +++ b/backend/src/main/com/bycrm/dto/ReportDTO.java @@ -0,0 +1,26 @@ +package com.bycrm.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * 报备请求 DTO + */ +@Data +public class ReportDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 客户ID + */ + @NotNull(message = "客户ID不能为空") + private Long customerId; + + /** + * 报备说明 + */ + private String description; +} diff --git a/backend/src/main/com/bycrm/entity/Customer.java b/backend/src/main/com/bycrm/entity/Customer.java new file mode 100644 index 0000000..c07ef02 --- /dev/null +++ b/backend/src/main/com/bycrm/entity/Customer.java @@ -0,0 +1,58 @@ +package com.bycrm.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 客户实体 + */ +@Data +public class Customer implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 客户ID + */ + private Long id; + + /** + * 客户名称 + */ + private String name; + + /** + * 联系电话 + */ + private String phone; + + /** + * 地址 + */ + private String address; + + /** + * 所属行业 + */ + private String industry; + + /** + * 状态:0-可报备 1-保护中 2-已失效 + */ + private Integer status; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; +} diff --git a/backend/src/main/com/bycrm/entity/Dealer.java b/backend/src/main/com/bycrm/entity/Dealer.java new file mode 100644 index 0000000..ad7c2c2 --- /dev/null +++ b/backend/src/main/com/bycrm/entity/Dealer.java @@ -0,0 +1,63 @@ +package com.bycrm.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 经销商实体 + */ +@Data +public class Dealer implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 经销商ID + */ + private Long id; + + /** + * 经销商名称 + */ + private String name; + + /** + * 经销商编码 + */ + private String code; + + /** + * 联系人 + */ + private String contactPerson; + + /** + * 联系电话 + */ + private String contactPhone; + + /** + * 邮箱 + */ + private String email; + + /** + * 状态:0-禁用 1-启用 + */ + private Integer status; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; +} diff --git a/backend/src/main/com/bycrm/entity/Dict.java b/backend/src/main/com/bycrm/entity/Dict.java new file mode 100644 index 0000000..3ce93a9 --- /dev/null +++ b/backend/src/main/com/bycrm/entity/Dict.java @@ -0,0 +1,54 @@ +package com.bycrm.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 数据字典实体 + */ +@Data +public class Dict implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 字典ID + */ + private Long id; + + /** + * 字典编码 + */ + private String dictCode; + + /** + * 字典名称 + */ + private String dictName; + + /** + * 描述 + */ + private String description; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; + + /** + * 字典项列表(不存储在数据库中) + */ + private transient List items; +} diff --git a/backend/src/main/com/bycrm/entity/DictItem.java b/backend/src/main/com/bycrm/entity/DictItem.java new file mode 100644 index 0000000..e863e99 --- /dev/null +++ b/backend/src/main/com/bycrm/entity/DictItem.java @@ -0,0 +1,53 @@ +package com.bycrm.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 数据字典项实体 + */ +@Data +public class DictItem implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 字典项ID + */ + private Long id; + + /** + * 字典ID + */ + private Long dictId; + + /** + * 字典项标签 + */ + private String itemLabel; + + /** + * 字典项值 + */ + private String itemValue; + + /** + * 排序 + */ + private Integer sortOrder; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; +} diff --git a/backend/src/main/com/bycrm/entity/OperationLog.java b/backend/src/main/com/bycrm/entity/OperationLog.java new file mode 100644 index 0000000..51f71fd --- /dev/null +++ b/backend/src/main/com/bycrm/entity/OperationLog.java @@ -0,0 +1,57 @@ +package com.bycrm.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 操作日志实体 + */ +@Data +public class OperationLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 日志ID + */ + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名(不存储在数据库中) + */ + private transient String username; + + /** + * 模块名称 + */ + private String module; + + /** + * 操作类型 + */ + private String operation; + + /** + * 操作描述 + */ + private String description; + + /** + * IP地址 + */ + private String ip; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; +} diff --git a/backend/src/main/com/bycrm/entity/Report.java b/backend/src/main/com/bycrm/entity/Report.java new file mode 100644 index 0000000..8c829a8 --- /dev/null +++ b/backend/src/main/com/bycrm/entity/Report.java @@ -0,0 +1,85 @@ +package com.bycrm.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 报备实体 + */ +@Data +public class Report implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 报备ID + */ + private Long id; + + /** + * 经销商ID + */ + private Long dealerId; + + /** + * 客户ID + */ + private Long customerId; + + /** + * 报备说明 + */ + private String description; + + /** + * 状态:0-待审核 1-已通过 2-已驳回 3-已失效 + */ + private Integer status; + + /** + * 驳回原因 + */ + private String rejectReason; + + /** + * 保护期开始时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime protectStartDate; + + /** + * 保护期结束时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime protectEndDate; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; + + /** + * 关联查询字段 - 经销商名称 + */ + private String dealerName; + + /** + * 关联查询字段 - 客户名称 + */ + private String customerName; + + /** + * 关联查询字段 - 客户电话 + */ + private String customerPhone; +} diff --git a/backend/src/main/com/bycrm/entity/User.java b/backend/src/main/com/bycrm/entity/User.java new file mode 100644 index 0000000..ca4e001 --- /dev/null +++ b/backend/src/main/com/bycrm/entity/User.java @@ -0,0 +1,73 @@ +package com.bycrm.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 用户实体 + */ +@Data +public class User implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long id; + + /** + * 用户名 + */ + private String username; + + /** + * 密码(BCrypt加密) + */ + private String password; + + /** + * 真实姓名 + */ + private String realName; + + /** + * 关联经销商ID(管理员为NULL) + */ + private Long dealerId; + + /** + * 角色:0-管理员 1-经销商用户 + */ + private Integer role; + + /** + * 状态:0-禁用 1-启用 + */ + private Integer status; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; + + /** + * 关联查询字段 - 经销商名称 + */ + private String dealerName; + + /** + * Token(不存储在数据库中,仅用于返回) + */ + private transient String token; +} diff --git a/backend/src/main/com/bycrm/exception/BusinessException.java b/backend/src/main/com/bycrm/exception/BusinessException.java new file mode 100644 index 0000000..a786fac --- /dev/null +++ b/backend/src/main/com/bycrm/exception/BusinessException.java @@ -0,0 +1,27 @@ +package com.bycrm.exception; + +import lombok.Getter; + +/** + * 业务异常 + */ +@Getter +public class BusinessException extends RuntimeException { + + private final Integer code; + + public BusinessException(String message) { + super(message); + this.code = 500; + } + + public BusinessException(Integer code, String message) { + super(message); + this.code = code; + } + + public BusinessException(String message, Throwable cause) { + super(message, cause); + this.code = 500; + } +} diff --git a/backend/src/main/com/bycrm/exception/GlobalExceptionHandler.java b/backend/src/main/com/bycrm/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..2262dd1 --- /dev/null +++ b/backend/src/main/com/bycrm/exception/GlobalExceptionHandler.java @@ -0,0 +1,62 @@ +package com.bycrm.exception; + +import com.bycrm.common.Result; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.stream.Collectors; + +/** + * 全局异常处理器 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 业务异常 + */ + @ExceptionHandler(BusinessException.class) + public Result handleBusinessException(BusinessException e) { + log.error("业务异常:{}", e.getMessage()); + return Result.error(e.getCode(), e.getMessage()); + } + + /** + * 参数校验异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数校验异常:{}", message); + return Result.error(400, message); + } + + /** + * 约束违反异常 + */ + @ExceptionHandler(ConstraintViolationException.class) + public Result handleConstraintViolationException(ConstraintViolationException e) { + String message = e.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(", ")); + log.error("约束违反异常:{}", message); + return Result.error(400, message); + } + + /** + * 系统异常 + */ + @ExceptionHandler(Exception.class) + public Result handleException(Exception e) { + log.error("系统异常", e); + return Result.error("系统异常,请联系管理员"); + } +} diff --git a/backend/src/main/com/bycrm/mapper/CustomerMapper.java b/backend/src/main/com/bycrm/mapper/CustomerMapper.java new file mode 100644 index 0000000..33f99da --- /dev/null +++ b/backend/src/main/com/bycrm/mapper/CustomerMapper.java @@ -0,0 +1,51 @@ +package com.bycrm.mapper; + +import com.bycrm.entity.Customer; +import com.bycrm.dto.PageQuery; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 客户 Mapper + */ +@Mapper +public interface CustomerMapper { + + /** + * 根据ID查询客户 + */ + Customer selectById(@Param("id") Long id); + + /** + * 根据名称模糊查询客户(用于客户唯一性校验) + */ + List selectByNameLike(@Param("name") String name); + + /** + * 分页查询客户 + */ + List selectPage(@Param("query") PageQuery query, @Param("name") String name, + @Param("industry") String industry, @Param("status") Integer status); + + /** + * 查询客户总数 + */ + Long countPage(@Param("name") String name, @Param("industry") String industry, @Param("status") Integer status); + + /** + * 插入客户 + */ + int insert(Customer customer); + + /** + * 更新客户 + */ + int update(Customer customer); + + /** + * 删除客户 + */ + int deleteById(@Param("id") Long id); +} diff --git a/backend/src/main/com/bycrm/mapper/DealerMapper.java b/backend/src/main/com/bycrm/mapper/DealerMapper.java new file mode 100644 index 0000000..5aa1b40 --- /dev/null +++ b/backend/src/main/com/bycrm/mapper/DealerMapper.java @@ -0,0 +1,44 @@ +package com.bycrm.mapper; + +import com.bycrm.entity.Dealer; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 经销商 Mapper + */ +@Mapper +public interface DealerMapper { + + /** + * 根据ID查询经销商 + */ + Dealer selectById(@Param("id") Long id); + + /** + * 根据编码查询经销商 + */ + Dealer selectByCode(@Param("code") String code); + + /** + * 查询所有经销商 + */ + List selectList(@Param("name") String name, @Param("code") String code, @Param("status") Integer status); + + /** + * 插入经销商 + */ + int insert(Dealer dealer); + + /** + * 更新经销商 + */ + int update(Dealer dealer); + + /** + * 删除经销商 + */ + int deleteById(@Param("id") Long id); +} diff --git a/backend/src/main/com/bycrm/mapper/ReportMapper.java b/backend/src/main/com/bycrm/mapper/ReportMapper.java new file mode 100644 index 0000000..96da49f --- /dev/null +++ b/backend/src/main/com/bycrm/mapper/ReportMapper.java @@ -0,0 +1,63 @@ +package com.bycrm.mapper; + +import com.bycrm.entity.Report; +import com.bycrm.dto.PageQuery; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 报备 Mapper + */ +@Mapper +public interface ReportMapper { + + /** + * 根据ID查询报备(含关联信息) + */ + Report selectById(@Param("id") Long id); + + /** + * 查询客户的有效报备 + */ + Report selectValidByCustomerId(@Param("customerId") Long customerId); + + /** + * 分页查询报备 + */ + List selectPage(@Param("query") PageQuery query, @Param("dealerId") Long dealerId, + @Param("dealerName") String dealerName, @Param("customerName") String customerName, + @Param("status") Integer status); + + /** + * 查询报备总数 + */ + Long countPage(@Param("dealerId") Long dealerId, @Param("dealerName") String dealerName, + @Param("customerName") String customerName, @Param("status") Integer status); + + /** + * 插入报备 + */ + int insert(Report report); + + /** + * 更新报备 + */ + int update(Report report); + + /** + * 删除报备 + */ + int deleteById(@Param("id") Long id); + + /** + * 查询即将过期的报备(定时任务使用) + */ + List selectExpiringReports(@Param("endDate") String endDate); + + /** + * 批量更新报备状态为已失效 + */ + int batchUpdateExpired(@Param("ids") List ids); +} diff --git a/backend/src/main/com/bycrm/mapper/UserMapper.java b/backend/src/main/com/bycrm/mapper/UserMapper.java new file mode 100644 index 0000000..d620a1e --- /dev/null +++ b/backend/src/main/com/bycrm/mapper/UserMapper.java @@ -0,0 +1,44 @@ +package com.bycrm.mapper; + +import com.bycrm.entity.User; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 用户 Mapper + */ +@Mapper +public interface UserMapper { + + /** + * 根据用户名查询用户 + */ + User selectByUsername(@Param("username") String username); + + /** + * 根据ID查询用户 + */ + User selectById(@Param("id") Long id); + + /** + * 查询所有用户 + */ + List selectList(); + + /** + * 插入用户 + */ + int insert(User user); + + /** + * 更新用户 + */ + int update(User user); + + /** + * 删除用户 + */ + int deleteById(@Param("id") Long id); +} diff --git a/backend/src/main/com/bycrm/service/CustomerService.java b/backend/src/main/com/bycrm/service/CustomerService.java new file mode 100644 index 0000000..46a78b4 --- /dev/null +++ b/backend/src/main/com/bycrm/service/CustomerService.java @@ -0,0 +1,50 @@ +package com.bycrm.service; + +import com.bycrm.common.PageResult; +import com.bycrm.dto.CustomerDTO; +import com.bycrm.dto.PageQuery; +import com.bycrm.entity.Customer; +import com.bycrm.vo.CustomerVO; + +import java.util.List; + +/** + * 客户服务接口 + */ +public interface CustomerService { + + /** + * 分页查询客户 + */ + PageResult getCustomerPage(PageQuery query, String name, String industry, Integer status); + + /** + * 根据 ID 获取客户详情 + */ + CustomerVO getCustomerById(Long id); + + /** + * 创建客户 + */ + Customer createCustomer(CustomerDTO customerDTO, Long currentUserId); + + /** + * 更新客户 + */ + void updateCustomer(Long id, CustomerDTO customerDTO, Long currentUserId); + + /** + * 删除客户 + */ + void deleteCustomer(Long id, Long currentUserId); + + /** + * 根据名称模糊查询客户(用于客户唯一性校验) + */ + List searchByName(String name); + + /** + * 校验客户名称是否重复 + */ + void checkCustomerNameDuplicate(String name); +} diff --git a/backend/src/main/com/bycrm/service/DealerService.java b/backend/src/main/com/bycrm/service/DealerService.java new file mode 100644 index 0000000..7dafa25 --- /dev/null +++ b/backend/src/main/com/bycrm/service/DealerService.java @@ -0,0 +1,37 @@ +package com.bycrm.service; + +import com.bycrm.dto.DealerDTO; +import com.bycrm.entity.Dealer; + +import java.util.List; + +/** + * 经销商服务接口 + */ +public interface DealerService { + + /** + * 查询所有经销商 + */ + List getDealerList(String name, String code, Integer status); + + /** + * 根据 ID 获取经销商 + */ + Dealer getDealerById(Long id); + + /** + * 创建经销商 + */ + void createDealer(DealerDTO dealerDTO); + + /** + * 更新经销商 + */ + void updateDealer(Long id, DealerDTO dealerDTO); + + /** + * 删除经销商 + */ + void deleteDealer(Long id); +} diff --git a/backend/src/main/com/bycrm/service/ReportService.java b/backend/src/main/com/bycrm/service/ReportService.java new file mode 100644 index 0000000..1eacc87 --- /dev/null +++ b/backend/src/main/com/bycrm/service/ReportService.java @@ -0,0 +1,44 @@ +package com.bycrm.service; + +import com.bycrm.common.PageResult; +import com.bycrm.dto.PageQuery; +import com.bycrm.dto.ReportAuditDTO; +import com.bycrm.dto.ReportDTO; +import com.bycrm.vo.ReportVO; + +/** + * 报备服务接口 + */ +public interface ReportService { + + /** + * 分页查询报备 + */ + PageResult getReportPage(PageQuery query, Long dealerId, String dealerName, + String customerName, Integer status, Long currentUserId); + + /** + * 根据 ID 获取报备详情 + */ + ReportVO getReportById(Long id); + + /** + * 创建报备 + */ + void createReport(ReportDTO reportDTO, Long currentUserId); + + /** + * 审核报备 + */ + void auditReport(Long id, ReportAuditDTO auditDTO, Long currentUserId); + + /** + * 撤回报备 + */ + void withdrawReport(Long id, Long currentUserId); + + /** + * 处理过期报备(定时任务调用) + */ + void handleExpiredReports(); +} diff --git a/backend/src/main/com/bycrm/service/UserService.java b/backend/src/main/com/bycrm/service/UserService.java new file mode 100644 index 0000000..1a7449f --- /dev/null +++ b/backend/src/main/com/bycrm/service/UserService.java @@ -0,0 +1,31 @@ +package com.bycrm.service; + +import com.bycrm.dto.LoginDTO; +import com.bycrm.entity.User; +import com.bycrm.vo.UserInfoVO; + +/** + * 用户服务接口 + */ +public interface UserService { + + /** + * 用户登录 + */ + String login(LoginDTO loginDTO); + + /** + * 根据 ID 获取用户信息 + */ + User getUserById(Long id); + + /** + * 根据用户名获取用户信息 + */ + User getUserByUsername(String username); + + /** + * 获取当前登录用户信息 + */ + UserInfoVO getCurrentUser(String token); +} diff --git a/backend/src/main/com/bycrm/service/impl/CustomerServiceImpl.java b/backend/src/main/com/bycrm/service/impl/CustomerServiceImpl.java new file mode 100644 index 0000000..6a2c2b7 --- /dev/null +++ b/backend/src/main/com/bycrm/service/impl/CustomerServiceImpl.java @@ -0,0 +1,152 @@ +package com.bycrm.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.bycrm.common.Constants; +import com.bycrm.common.PageResult; +import com.bycrm.dto.CustomerDTO; +import com.bycrm.dto.PageQuery; +import com.bycrm.entity.Customer; +import com.bycrm.entity.User; +import com.bycrm.exception.BusinessException; +import com.bycrm.mapper.CustomerMapper; +import com.bycrm.mapper.UserMapper; +import com.bycrm.service.CustomerService; +import com.bycrm.vo.CustomerVO; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 客户服务实现 + */ +@Service +public class CustomerServiceImpl implements CustomerService { + + private final CustomerMapper customerMapper; + private final UserMapper userMapper; + + public CustomerServiceImpl(CustomerMapper customerMapper, UserMapper userMapper) { + this.customerMapper = customerMapper; + this.userMapper = userMapper; + } + + @Override + public PageResult getCustomerPage(PageQuery query, String name, String industry, Integer status) { + // 计算偏移量 + query.setOffset((query.getCurrent() - 1) * query.getSize()); + + List customers = customerMapper.selectPage(query, name, industry, status); + Long total = customerMapper.countPage(name, industry, status); + + List voList = customers.stream().map(customer -> { + CustomerVO vo = convertToVO(customer); + return vo; + }).collect(Collectors.toList()); + + return PageResult.of(total, voList, query.getCurrent(), query.getSize()); + } + + @Override + public CustomerVO getCustomerById(Long id) { + Customer customer = customerMapper.selectById(id); + if (customer == null) { + throw new BusinessException("客户不存在"); + } + return convertToVO(customer); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Customer createCustomer(CustomerDTO customerDTO, Long currentUserId) { + // 校验客户名称是否重复 + checkCustomerNameDuplicate(customerDTO.getName()); + + Customer customer = new Customer(); + BeanUtils.copyProperties(customerDTO, customer); + customer.setStatus(Constants.CUSTOMER_STATUS_AVAILABLE); + customer.setCreatedAt(LocalDateTime.now()); + customer.setUpdatedAt(LocalDateTime.now()); + + customerMapper.insert(customer); + return customer; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCustomer(Long id, CustomerDTO customerDTO, Long currentUserId) { + Customer existingCustomer = customerMapper.selectById(id); + if (existingCustomer == null) { + throw new BusinessException("客户不存在"); + } + + // 如果修改了名称,需要检查新名称是否与其他客户重复 + if (!existingCustomer.getName().equals(customerDTO.getName())) { + checkCustomerNameDuplicate(customerDTO.getName()); + } + + Customer customer = new Customer(); + BeanUtils.copyProperties(customerDTO, customer); + customer.setId(id); + customer.setUpdatedAt(LocalDateTime.now()); + + customerMapper.update(customer); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteCustomer(Long id, Long currentUserId) { + Customer customer = customerMapper.selectById(id); + if (customer == null) { + throw new BusinessException("客户不存在"); + } + + if (customer.getStatus() == Constants.CUSTOMER_STATUS_PROTECTED) { + throw new BusinessException("客户处于保护期中,无法删除"); + } + + customerMapper.deleteById(id); + } + + @Override + public List searchByName(String name) { + return customerMapper.selectByNameLike(name); + } + + @Override + public void checkCustomerNameDuplicate(String name) { + List existingCustomers = customerMapper.selectByNameLike(name); + if (existingCustomers.stream().anyMatch(c -> c.getName().equals(name))) { + throw new BusinessException("客户名称已存在"); + } + } + + /** + * 转换为 VO + */ + private CustomerVO convertToVO(Customer customer) { + CustomerVO vo = new CustomerVO(); + BeanUtils.copyProperties(customer, vo); + + // 设置状态描述 + switch (customer.getStatus()) { + case Constants.CUSTOMER_STATUS_AVAILABLE: + vo.setStatusDesc("可报备"); + break; + case Constants.CUSTOMER_STATUS_PROTECTED: + vo.setStatusDesc("保护中"); + break; + case Constants.CUSTOMER_STATUS_EXPIRED: + vo.setStatusDesc("已失效"); + break; + default: + vo.setStatusDesc("未知"); + } + + return vo; + } +} diff --git a/backend/src/main/com/bycrm/service/impl/DealerServiceImpl.java b/backend/src/main/com/bycrm/service/impl/DealerServiceImpl.java new file mode 100644 index 0000000..0923eda --- /dev/null +++ b/backend/src/main/com/bycrm/service/impl/DealerServiceImpl.java @@ -0,0 +1,92 @@ +package com.bycrm.service.impl; + +import com.bycrm.dto.DealerDTO; +import com.bycrm.entity.Dealer; +import com.bycrm.exception.BusinessException; +import com.bycrm.mapper.DealerMapper; +import com.bycrm.service.DealerService; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 经销商服务实现 + */ +@Service +public class DealerServiceImpl implements DealerService { + + private final DealerMapper dealerMapper; + + public DealerServiceImpl(DealerMapper dealerMapper) { + this.dealerMapper = dealerMapper; + } + + @Override + public List getDealerList(String name, String code, Integer status) { + return dealerMapper.selectList(name, code, status); + } + + @Override + public Dealer getDealerById(Long id) { + Dealer dealer = dealerMapper.selectById(id); + if (dealer == null) { + throw new BusinessException("经销商不存在"); + } + return dealer; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void createDealer(DealerDTO dealerDTO) { + // 检查编码是否重复 + Dealer existingDealer = dealerMapper.selectByCode(dealerDTO.getCode()); + if (existingDealer != null) { + throw new BusinessException("经销商编码已存在"); + } + + Dealer dealer = new Dealer(); + BeanUtils.copyProperties(dealerDTO, dealer); + dealer.setCreatedAt(LocalDateTime.now()); + dealer.setUpdatedAt(LocalDateTime.now()); + + dealerMapper.insert(dealer); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDealer(Long id, DealerDTO dealerDTO) { + Dealer existingDealer = dealerMapper.selectById(id); + if (existingDealer == null) { + throw new BusinessException("经销商不存在"); + } + + // 如果修改了编码,需要检查新编码是否与其他经销商重复 + if (!existingDealer.getCode().equals(dealerDTO.getCode())) { + Dealer codeDealer = dealerMapper.selectByCode(dealerDTO.getCode()); + if (codeDealer != null && !codeDealer.getId().equals(id)) { + throw new BusinessException("经销商编码已存在"); + } + } + + Dealer dealer = new Dealer(); + BeanUtils.copyProperties(dealerDTO, dealer); + dealer.setId(id); + dealer.setUpdatedAt(LocalDateTime.now()); + + dealerMapper.update(dealer); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDealer(Long id) { + Dealer dealer = dealerMapper.selectById(id); + if (dealer == null) { + throw new BusinessException("经销商不存在"); + } + + dealerMapper.deleteById(id); + } +} diff --git a/backend/src/main/com/bycrm/service/impl/ReportServiceImpl.java b/backend/src/main/com/bycrm/service/impl/ReportServiceImpl.java new file mode 100644 index 0000000..f8bc8b7 --- /dev/null +++ b/backend/src/main/com/bycrm/service/impl/ReportServiceImpl.java @@ -0,0 +1,233 @@ +package com.bycrm.service.impl; + +import com.bycrm.common.Constants; +import com.bycrm.common.PageResult; +import com.bycrm.dto.PageQuery; +import com.bycrm.dto.ReportAuditDTO; +import com.bycrm.dto.ReportDTO; +import com.bycrm.entity.Report; +import com.bycrm.entity.Customer; +import com.bycrm.entity.User; +import com.bycrm.exception.BusinessException; +import com.bycrm.mapper.CustomerMapper; +import com.bycrm.mapper.ReportMapper; +import com.bycrm.mapper.UserMapper; +import com.bycrm.service.ReportService; +import com.bycrm.vo.ReportVO; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 报备服务实现 + */ +@Service +public class ReportServiceImpl implements ReportService { + + private final ReportMapper reportMapper; + private final CustomerMapper customerMapper; + private final UserMapper userMapper; + + @Value("${crm.report.ttl-days:90}") + private Integer protectDays; + + @Value("${crm.report.allow-overlap:false}") + private Boolean allowOverlap; + + public ReportServiceImpl(ReportMapper reportMapper, CustomerMapper customerMapper, UserMapper userMapper) { + this.reportMapper = reportMapper; + this.customerMapper = customerMapper; + this.userMapper = userMapper; + } + + @Override + public PageResult getReportPage(PageQuery query, Long dealerId, String dealerName, + String customerName, Integer status, Long currentUserId) { + // 获取当前用户 + User currentUser = userMapper.selectById(currentUserId); + + // 如果是经销商用户,只能查看自己的报备 + if (currentUser.getRole() == Constants.USER_ROLE_DEALER) { + dealerId = currentUser.getDealerId(); + } + + // 计算偏移量 + query.setOffset((query.getCurrent() - 1) * query.getSize()); + + List reports = reportMapper.selectPage(query, dealerId, dealerName, customerName, status); + Long total = reportMapper.countPage(dealerId, dealerName, customerName, status); + + List voList = reports.stream().map(this::convertToVO).collect(Collectors.toList()); + + return PageResult.of(total, voList, query.getCurrent(), query.getSize()); + } + + @Override + public ReportVO getReportById(Long id) { + Report report = reportMapper.selectById(id); + if (report == null) { + throw new BusinessException("报备不存在"); + } + return convertToVO(report); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void createReport(ReportDTO reportDTO, Long currentUserId) { + // 获取当前用户 + User currentUser = userMapper.selectById(currentUserId); + if (currentUser.getDealerId() == null) { + throw new BusinessException("您未关联经销商,无法提交报备"); + } + + // 检查客户是否存在 + Customer customer = customerMapper.selectById(reportDTO.getCustomerId()); + if (customer == null) { + throw new BusinessException("客户不存在"); + } + + // 防撞单校验:检查该客户是否已存在有效报备 + Report existingReport = reportMapper.selectValidByCustomerId(reportDTO.getCustomerId()); + if (existingReport != null && !allowOverlap) { + throw new BusinessException("该客户已被其他经销商报备,无法重复报备"); + } + + // 创建报备 + Report report = new Report(); + report.setDealerId(currentUser.getDealerId()); + report.setCustomerId(reportDTO.getCustomerId()); + report.setDescription(reportDTO.getDescription()); + report.setStatus(Constants.REPORT_STATUS_PENDING); + report.setCreatedAt(LocalDateTime.now()); + report.setUpdatedAt(LocalDateTime.now()); + + reportMapper.insert(report); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void auditReport(Long id, ReportAuditDTO auditDTO, Long currentUserId) { + // 获取当前用户 + User currentUser = userMapper.selectById(currentUserId); + if (currentUser.getRole() != Constants.USER_ROLE_ADMIN) { + throw new BusinessException("只有管理员才能审核报备"); + } + + Report report = reportMapper.selectById(id); + if (report == null) { + throw new BusinessException("报备不存在"); + } + + if (report.getStatus() != Constants.REPORT_STATUS_PENDING) { + throw new BusinessException("该报备已被审核,无需重复操作"); + } + + LocalDateTime now = LocalDateTime.now(); + + if (auditDTO.getApproved()) { + // 审核通过 + report.setStatus(Constants.REPORT_STATUS_APPROVED); + report.setProtectStartDate(now); + report.setProtectEndDate(now.plusDays(protectDays)); + + // 更新客户状态为保护中 + Customer customer = customerMapper.selectById(report.getCustomerId()); + customer.setStatus(Constants.CUSTOMER_STATUS_PROTECTED); + customerMapper.update(customer); + } else { + // 审核驳回 + report.setStatus(Constants.REPORT_STATUS_REJECTED); + report.setRejectReason(auditDTO.getRejectReason()); + } + + report.setUpdatedAt(now); + reportMapper.update(report); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void withdrawReport(Long id, Long currentUserId) { + User currentUser = userMapper.selectById(currentUserId); + + Report report = reportMapper.selectById(id); + if (report == null) { + throw new BusinessException("报备不存在"); + } + + // 只有报备提交者才能撤回 + if (!report.getDealerId().equals(currentUser.getDealerId())) { + throw new BusinessException("您只能撤回自己的报备"); + } + + if (report.getStatus() != Constants.REPORT_STATUS_PENDING) { + throw new BusinessException("只能撤回待审核的报备"); + } + + reportMapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void handleExpiredReports() { + // 查询所有保护期已到的报备 + String endDate = LocalDateTime.now().toString(); + List expiringReports = reportMapper.selectExpiringReports(endDate); + + if (expiringReports.isEmpty()) { + return; + } + + // 批量更新报备状态 + List ids = expiringReports.stream().map(Report::getId).collect(Collectors.toList()); + reportMapper.batchUpdateExpired(ids); + + // 批量更新客户状态为可报备 + expiringReports.forEach(report -> { + Customer customer = customerMapper.selectById(report.getCustomerId()); + if (customer != null && customer.getStatus() == Constants.CUSTOMER_STATUS_PROTECTED) { + customer.setStatus(Constants.CUSTOMER_STATUS_AVAILABLE); + customerMapper.update(customer); + } + }); + } + + /** + * 转换为 VO + */ + private ReportVO convertToVO(Report report) { + ReportVO vo = new ReportVO(); + BeanUtils.copyProperties(report, vo); + + // 设置状态描述 + switch (report.getStatus()) { + case Constants.REPORT_STATUS_PENDING: + vo.setStatusDesc("待审核"); + break; + case Constants.REPORT_STATUS_APPROVED: + vo.setStatusDesc("已通过"); + // 计算剩余保护天数 + if (report.getProtectEndDate() != null) { + long days = ChronoUnit.DAYS.between(LocalDateTime.now(), report.getProtectEndDate()); + vo.setRemainDays((int) Math.max(0, days)); + } + break; + case Constants.REPORT_STATUS_REJECTED: + vo.setStatusDesc("已驳回"); + break; + case Constants.REPORT_STATUS_EXPIRED: + vo.setStatusDesc("已失效"); + break; + default: + vo.setStatusDesc("未知"); + } + + return vo; + } +} diff --git a/backend/src/main/com/bycrm/service/impl/UserServiceImpl.java b/backend/src/main/com/bycrm/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..a759c29 --- /dev/null +++ b/backend/src/main/com/bycrm/service/impl/UserServiceImpl.java @@ -0,0 +1,74 @@ +package com.bycrm.service.impl; + +import com.bycrm.common.Constants; +import com.bycrm.entity.User; +import com.bycrm.exception.BusinessException; +import com.bycrm.mapper.UserMapper; +import com.bycrm.service.UserService; +import com.bycrm.util.JwtUtil; +import com.bycrm.vo.UserInfoVO; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +/** + * 用户服务实现 + */ +@Service +public class UserServiceImpl implements UserService { + + private final UserMapper userMapper; + private final JwtUtil jwtUtil; + private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + public UserServiceImpl(UserMapper userMapper, JwtUtil jwtUtil) { + this.userMapper = userMapper; + this.jwtUtil = jwtUtil; + } + + @Override + public String login(com.bycrm.dto.LoginDTO loginDTO) { + User user = userMapper.selectByUsername(loginDTO.getUsername()); + if (user == null) { + throw new BusinessException("用户名或密码错误"); + } + + if (user.getStatus() == Constants.USER_STATUS_DISABLED) { + throw new BusinessException("用户已被禁用"); + } + + if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { + throw new BusinessException("用户名或密码错误"); + } + + return jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole(), user.getDealerId()); + } + + @Override + public User getUserById(Long id) { + return userMapper.selectById(id); + } + + @Override + public User getUserByUsername(String username) { + return userMapper.selectByUsername(username); + } + + @Override + public UserInfoVO getCurrentUser(String token) { + Long userId = jwtUtil.getUserIdFromToken(token); + User user = userMapper.selectById(userId); + if (user == null) { + throw new BusinessException("用户不存在"); + } + + UserInfoVO vo = new UserInfoVO(); + vo.setUserId(user.getId()); + vo.setUsername(user.getUsername()); + vo.setRealName(user.getRealName()); + vo.setDealerId(user.getDealerId()); + vo.setDealerName(user.getDealerName()); + vo.setRole(user.getRole()); + vo.setRoleDesc(user.getRole() == Constants.USER_ROLE_ADMIN ? "管理员" : "经销商用户"); + return vo; + } +} diff --git a/backend/src/main/com/bycrm/task/ReportExpireTask.java b/backend/src/main/com/bycrm/task/ReportExpireTask.java new file mode 100644 index 0000000..bf74fdd --- /dev/null +++ b/backend/src/main/com/bycrm/task/ReportExpireTask.java @@ -0,0 +1,34 @@ +package com.bycrm.task; + +import com.bycrm.service.ReportService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 报备过期处理定时任务 + */ +@Slf4j +@Component +public class ReportExpireTask { + + private final ReportService reportService; + + public ReportExpireTask(ReportService reportService) { + this.reportService = reportService; + } + + /** + * 每天凌晨1点执行,处理过期的报备 + */ + @Scheduled(cron = "0 0 1 * * ?") + public void handleExpiredReports() { + log.info("开始处理过期报备..."); + try { + reportService.handleExpiredReports(); + log.info("过期报备处理完成"); + } catch (Exception e) { + log.error("处理过期报备失败", e); + } + } +} diff --git a/backend/src/main/com/bycrm/util/JwtUtil.java b/backend/src/main/com/bycrm/util/JwtUtil.java new file mode 100644 index 0000000..85b099a --- /dev/null +++ b/backend/src/main/com/bycrm/util/JwtUtil.java @@ -0,0 +1,128 @@ +package com.bycrm.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JWT 工具类 + */ +@Component +public class JwtUtil { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private Long expiration; + + /** + * 生成 Token + */ + public String generateToken(Long userId, String username, Integer role, Long dealerId) { + Map claims = new HashMap<>(); + claims.put("userId", userId); + claims.put("username", username); + claims.put("role", role); + claims.put("dealerId", dealerId); + return generateToken(claims); + } + + /** + * 生成 Token + */ + public String generateToken(Map claims) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expiration); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKey(), SignatureAlgorithm.HS512) + .compact(); + } + + /** + * 从 Token 中获取 Claims + */ + public Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从 Token 中获取用户ID + */ + public Long getUserIdFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.get("userId", Long.class); + } + + /** + * 从 Token 中获取用户名 + */ + public String getUsernameFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.get("username", String.class); + } + + /** + * 从 Token 中获取角色 + */ + public Integer getRoleFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.get("role", Integer.class); + } + + /** + * 从 Token 中获取经销商ID + */ + public Long getDealerIdFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.get("dealerId", Long.class); + } + + /** + * 验证 Token 是否过期 + */ + public boolean isTokenExpired(String token) { + try { + Date expiration = getClaimsFromToken(token).getExpiration(); + return expiration.before(new Date()); + } catch (Exception e) { + return true; + } + } + + /** + * 验证 Token + */ + public boolean validateToken(String token) { + try { + return !isTokenExpired(token); + } catch (Exception e) { + return false; + } + } + + /** + * 获取签名密钥 + */ + private SecretKey getSigningKey() { + byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/backend/src/main/com/bycrm/vo/CustomerVO.java b/backend/src/main/com/bycrm/vo/CustomerVO.java new file mode 100644 index 0000000..f01d7c4 --- /dev/null +++ b/backend/src/main/com/bycrm/vo/CustomerVO.java @@ -0,0 +1,79 @@ +package com.bycrm.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 客户详情 VO + */ +@Data +public class CustomerVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 客户ID + */ + private Long id; + + /** + * 客户名称 + */ + private String name; + + /** + * 联系电话 + */ + private String phone; + + /** + * 地址 + */ + private String address; + + /** + * 所属行业 + */ + private String industry; + + /** + * 状态:0-可报备 1-保护中 2-已失效 + */ + private Integer status; + + /** + * 状态描述 + */ + private String statusDesc; + + /** + * 当前报备经销商ID + */ + private Long currentDealerId; + + /** + * 当前报备经销商名称 + */ + private String currentDealerName; + + /** + * 保护期结束时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime protectEndDate; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; +} diff --git a/backend/src/main/com/bycrm/vo/ReportVO.java b/backend/src/main/com/bycrm/vo/ReportVO.java new file mode 100644 index 0000000..d3adf72 --- /dev/null +++ b/backend/src/main/com/bycrm/vo/ReportVO.java @@ -0,0 +1,95 @@ +package com.bycrm.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 报备详情 VO + */ +@Data +public class ReportVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 报备ID + */ + private Long id; + + /** + * 经销商ID + */ + private Long dealerId; + + /** + * 经销商名称 + */ + private String dealerName; + + /** + * 客户ID + */ + private Long customerId; + + /** + * 客户名称 + */ + private String customerName; + + /** + * 客户电话 + */ + private String customerPhone; + + /** + * 报备说明 + */ + private String description; + + /** + * 状态:0-待审核 1-已通过 2-已驳回 3-已失效 + */ + private Integer status; + + /** + * 状态描述 + */ + private String statusDesc; + + /** + * 驳回原因 + */ + private String rejectReason; + + /** + * 保护期开始时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime protectStartDate; + + /** + * 保护期结束时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime protectEndDate; + + /** + * 剩余保护天数 + */ + private Integer remainDays; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; +} diff --git a/backend/src/main/com/bycrm/vo/UserInfoVO.java b/backend/src/main/com/bycrm/vo/UserInfoVO.java new file mode 100644 index 0000000..e3b91ab --- /dev/null +++ b/backend/src/main/com/bycrm/vo/UserInfoVO.java @@ -0,0 +1,49 @@ +package com.bycrm.vo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户信息 VO + */ +@Data +public class UserInfoVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 真实姓名 + */ + private String realName; + + /** + * 经销商ID + */ + private Long dealerId; + + /** + * 经销商名称 + */ + private String dealerName; + + /** + * 角色:0-管理员 1-经销商用户 + */ + private Integer role; + + /** + * 角色描述 + */ + private String roleDesc; +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..998668a --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,75 @@ +server: + port: 8080 + servlet: + context-path: /api + +spring: + application: + name: by-crm + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/by_crm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + username: root + password: root + druid: + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 60000 + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + validation-query: SELECT 1 + test-while-idle: true + test-on-borrow: false + test-on-return: false + filters: stat,wall + web-stat-filter: + enabled: true + url-pattern: /* + stat-view-servlet: + enabled: true + url-pattern: /druid/* + reset-enable: false + login-username: admin + login-password: admin123 + + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + default-property-inclusion: non_null + +# MyBatis 配置 +mybatis: + mapper-locations: classpath:mapper/*.xml + type-aliases-package: com.bycrm.entity + configuration: + map-underscore-to-camel-case: true + cache-enabled: false + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +# JWT 配置 +jwt: + secret: by-crm-secret-key-2024-please-change-in-production + expiration: 86400000 # 24小时,单位:毫秒 + +# CRM 业务配置 +crm: + report: + ttl-days: 90 # 保护期天数 + allow-overlap: false # 是否允许重叠报备(生产环境必须为 false) + +# Swagger 配置 +springfox: + documentation: + swagger-ui: + enabled: true + enabled: true + +# 日志配置 +logging: + level: + com.bycrm.mapper: debug + org.springframework.web: info + pattern: + console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n' diff --git a/backend/src/main/resources/mapper/CustomerMapper.xml b/backend/src/main/resources/mapper/CustomerMapper.xml new file mode 100644 index 0000000..ddfe881 --- /dev/null +++ b/backend/src/main/resources/mapper/CustomerMapper.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + SELECT * FROM crm_customer WHERE id = #{id} + + + + SELECT * FROM crm_customer + WHERE name LIKE CONCAT('%', #{name}, '%') + LIMIT 10 + + + + SELECT * FROM crm_customer + + + AND name LIKE CONCAT('%', #{name}, '%') + + + AND industry = #{industry} + + + AND status = #{status} + + + ORDER BY created_at DESC + LIMIT #{query.size} OFFSET #{query.offset} + + + + SELECT COUNT(*) FROM crm_customer + + + AND name LIKE CONCAT('%', #{name}, '%') + + + AND industry = #{industry} + + + AND status = #{status} + + + + + + INSERT INTO crm_customer (name, phone, address, industry, status) + VALUES (#{name}, #{phone}, #{address}, #{industry}, #{status}) + + + + UPDATE crm_customer + + name = #{name}, + phone = #{phone}, + address = #{address}, + industry = #{industry}, + status = #{status}, + + WHERE id = #{id} + + + + DELETE FROM crm_customer WHERE id = #{id} + + + diff --git a/backend/src/main/resources/mapper/DealerMapper.xml b/backend/src/main/resources/mapper/DealerMapper.xml new file mode 100644 index 0000000..15e96c4 --- /dev/null +++ b/backend/src/main/resources/mapper/DealerMapper.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + SELECT * FROM crm_dealer WHERE id = #{id} + + + + SELECT * FROM crm_dealer WHERE code = #{code} + + + + SELECT * FROM crm_dealer + + + AND name LIKE CONCAT('%', #{name}, '%') + + + AND code LIKE CONCAT('%', #{code}, '%') + + + AND status = #{status} + + + ORDER BY created_at DESC + + + + INSERT INTO crm_dealer (name, code, contact_person, contact_phone, email, status) + VALUES (#{name}, #{code}, #{contactPerson}, #{contactPhone}, #{email}, #{status}) + + + + UPDATE crm_dealer + + name = #{name}, + code = #{code}, + contact_person = #{contactPerson}, + contact_phone = #{contactPhone}, + email = #{email}, + status = #{status}, + + WHERE id = #{id} + + + + DELETE FROM crm_dealer WHERE id = #{id} + + + diff --git a/backend/src/main/resources/mapper/ReportMapper.xml b/backend/src/main/resources/mapper/ReportMapper.xml new file mode 100644 index 0000000..8e8bdcd --- /dev/null +++ b/backend/src/main/resources/mapper/ReportMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + SELECT r.*, + d.name AS dealer_name, + c.name AS customer_name, + c.phone AS customer_phone + FROM crm_report r + LEFT JOIN crm_dealer d ON r.dealer_id = d.id + LEFT JOIN crm_customer c ON r.customer_id = c.id + WHERE r.id = #{id} + + + + SELECT * FROM crm_report + WHERE customer_id = #{customerId} + AND status IN (0, 1) + LIMIT 1 + + + + SELECT r.*, + d.name AS dealer_name, + c.name AS customer_name, + c.phone AS customer_phone + FROM crm_report r + LEFT JOIN crm_dealer d ON r.dealer_id = d.id + LEFT JOIN crm_customer c ON r.customer_id = c.id + + + AND r.dealer_id = #{dealerId} + + + AND d.name LIKE CONCAT('%', #{dealerName}, '%') + + + AND c.name LIKE CONCAT('%', #{customerName}, '%') + + + AND r.status = #{status} + + + ORDER BY r.created_at DESC + LIMIT #{query.size} OFFSET #{query.offset} + + + + SELECT COUNT(*) + FROM crm_report r + LEFT JOIN crm_dealer d ON r.dealer_id = d.id + LEFT JOIN crm_customer c ON r.customer_id = c.id + + + AND r.dealer_id = #{dealerId} + + + AND d.name LIKE CONCAT('%', #{dealerName}, '%') + + + AND c.name LIKE CONCAT('%', #{customerName}, '%') + + + AND r.status = #{status} + + + + + + INSERT INTO crm_report (dealer_id, customer_id, description, status, protect_start_date, protect_end_date) + VALUES (#{dealerId}, #{customerId}, #{description}, #{status}, #{protectStartDate}, #{protectEndDate}) + + + + UPDATE crm_report + + status = #{status}, + reject_reason = #{rejectReason}, + protect_start_date = #{protectStartDate}, + protect_end_date = #{protectEndDate}, + + WHERE id = #{id} + + + + DELETE FROM crm_report WHERE id = #{id} + + + + SELECT * FROM crm_report + WHERE status = 1 + AND protect_end_date <= #{endDate} + + + + UPDATE crm_report + SET status = 3 + WHERE id IN + + #{id} + + + + diff --git a/backend/src/main/resources/mapper/UserMapper.xml b/backend/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..efc1d3c --- /dev/null +++ b/backend/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + SELECT u.*, + d.name AS dealer_name + FROM crm_user u + LEFT JOIN crm_dealer d ON u.dealer_id = d.id + WHERE u.username = #{username} + + + + SELECT u.*, + d.name AS dealer_name + FROM crm_user u + LEFT JOIN crm_dealer d ON u.dealer_id = d.id + WHERE u.id = #{id} + + + + SELECT u.*, + d.name AS dealer_name + FROM crm_user u + LEFT JOIN crm_dealer d ON u.dealer_id = d.id + ORDER BY u.created_at DESC + + + + INSERT INTO crm_user (username, password, real_name, dealer_id, role, status) + VALUES (#{username}, #{password}, #{realName}, #{dealerId}, #{role}, #{status}) + + + + UPDATE crm_user + + password = #{password}, + real_name = #{realName}, + dealer_id = #{dealerId}, + role = #{role}, + status = #{status}, + + WHERE id = #{id} + + + + DELETE FROM crm_user WHERE id = #{id} + + + diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..66b62fe --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,25 @@ +module.exports = { + root: true, + env: { + node: true, + browser: true, + es2021: true + }, + extends: [ + 'eslint:recommended', + 'plugin:vue/vue3-recommended', + 'plugin:@typescript-eslint/recommended' + ], + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 2021, + parser: '@typescript-eslint/parser', + sourceType: 'module' + }, + plugins: ['vue', '@typescript-eslint'], + rules: { + 'vue/multi-word-component-names': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + 'vue/no-v-html': 'off' + } +} diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..9b0d7d1 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 100, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "lf" +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0f0d288 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + 经销商管理系统 + + + + + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..fcb49da --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "by-crm-frontend", + "version": "1.0.0", + "description": "经销商管理系统前端", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + }, + "dependencies": { + "vue": "^3.4.0", + "vue-router": "^4.2.0", + "pinia": "^2.1.0", + "pinia-plugin-persistedstate": "^3.2.0", + "axios": "^1.6.0", + "element-plus": "^2.5.0", + "@element-plus/icons-vue": "^2.3.0", + "dayjs": "^1.11.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0", + "vue-tsc": "^1.8.0", + "typescript": "^5.3.0", + "@types/node": "^20.10.0", + "eslint": "^8.55.0", + "eslint-plugin-vue": "^9.19.0", + "@typescript-eslint/parser": "^6.15.0", + "@typescript-eslint/eslint-plugin": "^6.15.0", + "prettier": "^3.1.0", + "sass": "^1.69.0" + }, + "engines": { + "node": ">=16.0.0", + "pnpm": ">=8.0.0" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..1d1385b --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,25 @@ + + + + + + + diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 0000000..1de877e --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,17 @@ +import { http } from '@/utils/request' +import type { LoginRequest, LoginResponse, User } from '@/types' + +// 用户登录 +export const login = (data: LoginRequest) => { + return http.post('/auth/login', data) +} + +// 获取当前用户信息 +export const getUserInfo = () => { + return http.get('/auth/user/info') +} + +// 用户退出 +export const logout = () => { + return http.post('/auth/logout') +} diff --git a/frontend/src/api/customer.ts b/frontend/src/api/customer.ts new file mode 100644 index 0000000..c1017e5 --- /dev/null +++ b/frontend/src/api/customer.ts @@ -0,0 +1,32 @@ +import { http } from '@/utils/request' +import type { Customer, CustomerForm, PageQuery, PageResult } from '@/types' + +// 分页查询客户 +export const getCustomerPage = (params: PageQuery & { name?: string; industry?: string; status?: number }) => { + return http.get>('/customer/page', { params }) +} + +// 根据ID获取客户详情 +export const getCustomerById = (id: number) => { + return http.get(`/customer/${id}`) +} + +// 创建客户 +export const createCustomer = (data: CustomerForm) => { + return http.post('/customer', data) +} + +// 更新客户 +export const updateCustomer = (id: number, data: CustomerForm) => { + return http.put(`/customer/${id}`, data) +} + +// 删除客户 +export const deleteCustomer = (id: number) => { + return http.delete(`/customer/${id}`) +} + +// 根据名称搜索客户 +export const searchCustomerByName = (name: string) => { + return http.get('/customer/search', { params: { name } }) +} diff --git a/frontend/src/api/dealer.ts b/frontend/src/api/dealer.ts new file mode 100644 index 0000000..823dfac --- /dev/null +++ b/frontend/src/api/dealer.ts @@ -0,0 +1,27 @@ +import { http } from '@/utils/request' +import type { Dealer } from '@/types' + +// 查询所有经销商 +export const getDealerList = (params?: { name?: string; code?: string; status?: number }) => { + return http.get('/dealer/list', { params }) +} + +// 根据ID获取经销商 +export const getDealerById = (id: number) => { + return http.get(`/dealer/${id}`) +} + +// 创建经销商 +export const createDealer = (data: Partial) => { + return http.post('/dealer', data) +} + +// 更新经销商 +export const updateDealer = (id: number, data: Partial) => { + return http.put(`/dealer/${id}`, data) +} + +// 删除经销商 +export const deleteDealer = (id: number) => { + return http.delete(`/dealer/${id}`) +} diff --git a/frontend/src/api/report.ts b/frontend/src/api/report.ts new file mode 100644 index 0000000..e371b4e --- /dev/null +++ b/frontend/src/api/report.ts @@ -0,0 +1,32 @@ +import { http } from '@/utils/request' +import type { Report, ReportForm, ReportAuditForm, PageQuery, PageResult } from '@/types' + +// 分页查询报备 +export const getReportPage = (params: PageQuery & { + dealerId?: number + dealerName?: string + customerName?: string + status?: number +}) => { + return http.get>('/report/page', { params }) +} + +// 根据ID获取报备详情 +export const getReportById = (id: number) => { + return http.get(`/report/${id}`) +} + +// 创建报备 +export const createReport = (data: ReportForm) => { + return http.post('/report', data) +} + +// 审核报备 +export const auditReport = (id: number, data: ReportAuditForm) => { + return http.put(`/report/${id}/audit`, data) +} + +// 撤回报备 +export const withdrawReport = (id: number) => { + return http.delete(`/report/${id}`) +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..aa806e3 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,24 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import App from './App.vue' +import router from './router' + +const app = createApp(App) +const pinia = createPinia() + +pinia.use(piniaPluginPersistedstate) + +app.use(pinia) +app.use(router) +app.use(ElementPlus) + +// 注册所有图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..88537e8 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,82 @@ +import { createRouter, createWebHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' +import { useUserStore } from '@/stores/user' + +const routes: RouteRecordRaw[] = [ + { + path: '/login', + name: 'Login', + component: () => import('@/views/Login.vue'), + meta: { title: '登录' } + }, + { + path: '/', + component: () => import('@/views/Layout.vue'), + redirect: '/dashboard', + children: [ + { + path: 'dashboard', + name: 'Dashboard', + component: () => import('@/views/Dashboard.vue'), + meta: { title: '首页' } + }, + { + path: 'customer', + name: 'Customer', + component: () => import('@/views/Customer.vue'), + meta: { title: '客户管理' } + }, + { + path: 'report', + name: 'Report', + component: () => import('@/views/Report.vue'), + meta: { title: '报备管理' } + }, + { + path: 'dealer', + name: 'Dealer', + component: () => import('@/views/Dealer.vue'), + meta: { title: '经销商管理', requiresAdmin: true } + } + ] + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 路由守卫 +router.beforeEach((to, from, next) => { + const userStore = useUserStore() + + // 设置页面标题 + document.title = `${to.meta.title || ''} - 经销商管理系统` + + // 如果访问登录页,已登录则跳转到首页 + if (to.path === '/login') { + if (userStore.isLoggedIn) { + next('/') + } else { + next() + } + return + } + + // 其他页面需要登录 + if (!userStore.isLoggedIn) { + next('/login') + return + } + + // 管理员权限校验 + if (to.meta.requiresAdmin && !userStore.isAdmin) { + next('/') + return + } + + next() +}) + +export default router diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts new file mode 100644 index 0000000..33e11f7 --- /dev/null +++ b/frontend/src/stores/user.ts @@ -0,0 +1,57 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { login as loginApi, getUserInfo, logout as logoutApi } from '@/api/auth' +import type { LoginRequest, User } from '@/types' + +export const useUserStore = defineStore( + 'user', + () => { + const token = ref(localStorage.getItem('token') || '') + const userInfo = ref(null) + + const isLoggedIn = computed(() => !!token.value) + const isAdmin = computed(() => userInfo.value?.role === 0) + const isDealer = computed(() => userInfo.value?.role === 1) + + // 登录 + const login = async (loginForm: LoginRequest) => { + const res = await loginApi(loginForm) + token.value = res.token + localStorage.setItem('token', res.token) + await fetchUserInfo() + } + + // 获取用户信息 + const fetchUserInfo = async () => { + if (token.value) { + try { + userInfo.value = await getUserInfo() + } catch (error) { + console.error('获取用户信息失败', error) + logout() + } + } + } + + // 退出登录 + const logout = () => { + token.value = '' + userInfo.value = null + localStorage.removeItem('token') + } + + return { + token, + userInfo, + isLoggedIn, + isAdmin, + isDealer, + login, + fetchUserInfo, + logout + } + }, + { + persist: true + } +) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..c8be8c7 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,101 @@ +// 用户相关类型 +export interface User { + userId: number + username: string + realName: string + dealerId?: number + dealerName?: string + role: number + roleDesc: string +} + +export interface LoginRequest { + username: string + password: string +} + +export interface LoginResponse { + token: string +} + +// 客户相关类型 +export interface Customer { + id: number + name: string + phone?: string + address?: string + industry?: string + status: number + statusDesc: string + createdAt: string + updatedAt: string +} + +export interface CustomerForm { + name: string + phone?: string + address?: string + industry?: string +} + +// 经销商相关类型 +export interface Dealer { + id: number + name: string + code: string + contactPerson: string + contactPhone: string + email?: string + status: number + createdAt: string + updatedAt: string +} + +// 报备相关类型 +export interface Report { + id: number + dealerId: number + dealerName: string + customerId: number + customerName: string + customerPhone?: string + description?: string + status: number + statusDesc: string + rejectReason?: string + protectStartDate?: string + protectEndDate?: string + remainDays?: number + createdAt: string + updatedAt: string +} + +export interface ReportForm { + customerId: number + description?: string +} + +export interface ReportAuditForm { + approved: boolean + rejectReason?: string +} + +// 分页相关类型 +export interface PageQuery { + current: number + size: number +} + +export interface PageResult { + total: number + records: T[] + current: number + size: number + pages: number +} + +// 字典相关类型 +export interface DictItem { + label: string + value: string | number +} diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts new file mode 100644 index 0000000..216a572 --- /dev/null +++ b/frontend/src/utils/request.ts @@ -0,0 +1,78 @@ +import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios' +import { ElMessage } from 'element-plus' +import { useUserStore } from '@/stores/user' + +// 响应数据接口 +export interface ApiResponse { + code: number + message: string + data: T + timestamp: number +} + +// 创建 axios 实例 +const service: AxiosInstance = axios.create({ + baseURL: '/api', + timeout: 30000, + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + } +}) + +// 请求拦截器 +service.interceptors.request.use( + (config) => { + const userStore = useUserStore() + if (userStore.token) { + config.headers.Authorization = `Bearer ${userStore.token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +service.interceptors.response.use( + (response: AxiosResponse) => { + const { data } = response + if (data.code === 200) { + return data.data + } else if (data.code === 401) { + ElMessage.error('登录已过期,请重新登录') + const userStore = useUserStore() + userStore.logout() + window.location.href = '/login' + return Promise.reject(new Error(data.message || '未授权')) + } else { + ElMessage.error(data.message || '请求失败') + return Promise.reject(new Error(data.message || '请求失败')) + } + }, + (error) => { + ElMessage.error(error.message || '网络请求失败') + return Promise.reject(error) + } +) + +// 封装请求方法 +export const http = { + get(url: string, config?: AxiosRequestConfig): Promise { + return service.get(url, config) + }, + + post(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return service.post(url, data, config) + }, + + put(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return service.put(url, data, config) + }, + + delete(url: string, config?: AxiosRequestConfig): Promise { + return service.delete(url, config) + } +} + +export default service diff --git a/frontend/src/views/Customer.vue b/frontend/src/views/Customer.vue new file mode 100644 index 0000000..80c046e --- /dev/null +++ b/frontend/src/views/Customer.vue @@ -0,0 +1,273 @@ + + + + + + 客户管理 + + + 新增客户 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 查询 + + 重置 + + + + + + + + + + + {{ getIndustryLabel(row.industry) }} + + + + + 可报备 + 保护中 + 已失效 + + + + + + 编辑 + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 确定 + + + + + + + + diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..c65f622 --- /dev/null +++ b/frontend/src/views/Dashboard.vue @@ -0,0 +1,152 @@ + + + + + + + + + + + 客户总数 + {{ stats.customerCount }} + + + + + + + + + + + + 报备总数 + {{ stats.reportCount }} + + + + + + + + + + + + 待审核 + {{ stats.pendingCount }} + + + + + + + + + + + + 经销商数 + {{ stats.dealerCount }} + + + + + + + + 欢迎使用经销商管理系统 + 您是管理员,可以管理所有客户、报备和经销商信息。 + 您是经销商用户,可以管理客户和提交报备申请。 + + + + + + + diff --git a/frontend/src/views/Dealer.vue b/frontend/src/views/Dealer.vue new file mode 100644 index 0000000..707e597 --- /dev/null +++ b/frontend/src/views/Dealer.vue @@ -0,0 +1,237 @@ + + + + + + 经销商管理 + + + 新增经销商 + + + + + + + + + + + + + + + + + + + + + + 查询 + + 重置 + + + + + + + + + + + + + + {{ row.status === 1 ? '启用' : '禁用' }} + + + + + + + 编辑 + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + + 启用 + 禁用 + + + + + 取消 + 确定 + + + + + + + + diff --git a/frontend/src/views/Layout.vue b/frontend/src/views/Layout.vue new file mode 100644 index 0000000..8e78301 --- /dev/null +++ b/frontend/src/views/Layout.vue @@ -0,0 +1,142 @@ + + + + 经销商管理系统 + + + + 首页 + + + + 客户管理 + + + + 报备管理 + + + + 经销商管理 + + + + + + + + + {{ currentPageTitle }} + + + + + + + {{ userStore.userInfo?.realName }} + + 管理员 + + 经销商 + + + + + + 退出登录 + + + + + + + + + + + + + + + + + diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..e6c15db --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,106 @@ + + + + 经销商管理系统 + + + + + + + + + + 登录 + + + + + + + + + + diff --git a/frontend/src/views/Report.vue b/frontend/src/views/Report.vue new file mode 100644 index 0000000..a171dcd --- /dev/null +++ b/frontend/src/views/Report.vue @@ -0,0 +1,348 @@ + + + + + + 报备管理 + + + 提交报备 + + + + + + + + + + + + + + + + + + + + + 查询 + + 重置 + + + + + + + + + + + + 待审核 + 已通过 + 已驳回 + 已失效 + + + + + {{ row.protectEndDate || '-' }} + + + + + + 查看 + + 审核 + + + 撤回 + + + + + + + + + + + + + + + + + + + + + + + 取消 + 提交 + + + + + + + + + 通过 + 驳回 + + + + + + + + 取消 + 确定 + + + + + + + + diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..323c78a --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..45d8ec7 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path alias */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..0d58693 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true + } + } + }, + build: { + outDir: 'dist', + sourcemap: false, + rollupOptions: { + output: { + manualChunks: { + 'element-plus': ['element-plus'], + 'vue-vendor': ['vue', 'vue-router', 'pinia'] + } + } + } + } +}) diff --git a/sql/init.sql b/sql/init.sql new file mode 100644 index 0000000..ad5026e --- /dev/null +++ b/sql/init.sql @@ -0,0 +1,161 @@ +-- 经销商管理系统数据库初始化脚本 +-- 数据库版本:MySQL 8.0 + +CREATE DATABASE IF NOT EXISTS by_crm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE by_crm; + +-- 经销商表 +CREATE TABLE IF NOT EXISTS crm_dealer ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '经销商ID', + name VARCHAR(100) NOT NULL COMMENT '经销商名称', + code VARCHAR(50) NOT NULL UNIQUE COMMENT '经销商编码', + contact_person VARCHAR(50) NOT NULL COMMENT '联系人', + contact_phone VARCHAR(20) NOT NULL COMMENT '联系电话', + email VARCHAR(100) COMMENT '邮箱', + status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用 1-启用', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_code (code), + INDEX idx_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='经销商表'; + +-- 客户表 +CREATE TABLE IF NOT EXISTS crm_customer ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '客户ID', + name VARCHAR(100) NOT NULL COMMENT '客户名称', + phone VARCHAR(20) COMMENT '联系电话', + address VARCHAR(255) COMMENT '地址', + industry VARCHAR(100) COMMENT '所属行业', + status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-可报备 1-保护中 2-已失效', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_name (name), + INDEX idx_status (status), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户表'; + +-- 报备表 +CREATE TABLE IF NOT EXISTS crm_report ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '报备ID', + dealer_id BIGINT NOT NULL COMMENT '经销商ID', + customer_id BIGINT NOT NULL COMMENT '客户ID', + description VARCHAR(500) COMMENT '报备说明', + status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待审核 1-已通过 2-已驳回 3-已失效', + reject_reason VARCHAR(255) COMMENT '驳回原因', + protect_start_date DATETIME COMMENT '保护期开始时间', + protect_end_date DATETIME COMMENT '保护期结束时间', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_dealer_id (dealer_id), + INDEX idx_customer_id (customer_id), + INDEX idx_status (status), + INDEX idx_protect_end_date (protect_end_date), + UNIQUE KEY uk_customer_valid (customer_id, status) COMMENT '同一客户只能有一个有效报备' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='报备表'; + +-- 用户表(管理员和经销商用户) +CREATE TABLE IF NOT EXISTS crm_user ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', + username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', + password VARCHAR(255) NOT NULL COMMENT '密码(BCrypt加密)', + real_name VARCHAR(50) NOT NULL COMMENT '真实姓名', + dealer_id BIGINT COMMENT '关联经销商ID(管理员为NULL)', + role TINYINT NOT NULL COMMENT '角色:0-管理员 1-经销商用户', + status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用 1-启用', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_username (username), + INDEX idx_dealer_id (dealer_id), + INDEX idx_role (role) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; + +-- 数据字典表 +CREATE TABLE IF NOT EXISTS crm_dict ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '字典ID', + dict_code VARCHAR(50) NOT NULL UNIQUE COMMENT '字典编码', + dict_name VARCHAR(100) NOT NULL COMMENT '字典名称', + description VARCHAR(255) COMMENT '描述', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据字典表'; + +-- 数据字典项表 +CREATE TABLE IF NOT EXISTS crm_dict_item ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '字典项ID', + dict_id BIGINT NOT NULL COMMENT '字典ID', + item_label VARCHAR(100) NOT NULL COMMENT '字典项标签', + item_value VARCHAR(50) NOT NULL COMMENT '字典项值', + sort_order INT NOT NULL DEFAULT 0 COMMENT '排序', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_dict_id (dict_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据字典项表'; + +-- 操作日志表 +CREATE TABLE IF NOT EXISTS crm_operation_log ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '日志ID', + user_id BIGINT NOT NULL COMMENT '用户ID', + module VARCHAR(50) NOT NULL COMMENT '模块名称', + operation VARCHAR(50) NOT NULL COMMENT '操作类型', + description VARCHAR(500) COMMENT '操作描述', + ip VARCHAR(50) COMMENT 'IP地址', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + INDEX idx_user_id (user_id), + INDEX idx_module (module), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表'; + +-- 插入初始数据 + +-- 插入默认管理员(用户名:admin,密码:admin123,BCrypt加密后的值) +INSERT INTO crm_user (username, password, real_name, dealer_id, role, status) VALUES +('admin', '$2a$10$ZK5LlFqFZ5.LpFJj/YqxJ.X5JD4JlSfHz.FGG8XnP/YjV6LvVJz0q', '系统管理员', NULL, 0, 1); + +-- 插入测试经销商 +INSERT INTO crm_dealer (name, code, contact_person, contact_phone, email, status) VALUES +('北京科技有限公司', 'DLR001', '张三', '13800138001', 'zhangsan@example.com', 1), +('上海贸易有限公司', 'DLR002', '李四', '13800138002', 'lisi@example.com', 1), +('深圳实业有限公司', 'DLR003', '王五', '13800138003', 'wangwu@example.com', 1); + +-- 插入测试经销商用户 +INSERT INTO crm_user (username, password, real_name, dealer_id, role, status) VALUES +('user001', '$2a$10$ZK5LlFqFZ5.LpFJj/YqxJ.X5JD4JlSfHz.FGG8XnP/YjV6LvVJz0q', '张三', 1, 1, 1), +('user002', '$2a$10$ZK5LlFqFZ5.LpFJj/YqxJ.X5JD4JlSfHz.FGG8XnP/YjV6LvVJz0q', '李四', 2, 1, 1), +('user003', '$2a$10$ZK5LlFqFZ5.LpFJj/YqxJ.X5JD4JlSfHz.FGG8XnP/YjV6LvVJz0q', '王五', 3, 1, 1); + +-- 插入数据字典 +INSERT INTO crm_dict (dict_code, dict_name, description) VALUES +('customer_status', '客户状态', '客户的状态信息'), +('report_status', '报备状态', '报备单的状态信息'), +('customer_industry', '客户行业', '客户所属行业分类'); + +-- 插入客户状态字典项 +INSERT INTO crm_dict_item (dict_id, item_label, item_value, sort_order) VALUES +(1, '可报备', '0', 0), +(1, '保护中', '1', 1), +(1, '已失效', '2', 2); + +-- 插入报备状态字典项 +INSERT INTO crm_dict_item (dict_id, item_label, item_value, sort_order) VALUES +(2, '待审核', '0', 0), +(2, '已通过', '1', 1), +(2, '已驳回', '2', 2), +(2, '已失效', '3', 3); + +-- 插入客户行业字典项 +INSERT INTO crm_dict_item (dict_id, item_label, item_value, sort_order) VALUES +(3, '制造业', 'manufacturing', 0), +(3, '互联网', 'internet', 1), +(3, '金融', 'finance', 2), +(3, '零售', 'retail', 3), +(3, '教育', 'education', 4), +(3, '医疗', 'healthcare', 5), +(3, '其他', 'other', 6); + +-- 插入测试客户 +INSERT INTO crm_customer (name, phone, address, industry, status) VALUES +('阿里巴巴集团', '0571-12345678', '浙江省杭州市余杭区', 'internet', 0), +('腾讯科技', '0755-87654321', '广东省深圳市南山区', 'internet', 0), +('华为技术', '0755-12345678', '广东省深圳市龙岗区', 'manufacturing', 0), +('京东集团', '010-66666666', '北京市大兴区', 'retail', 0), +('平安保险', '0755-55555555', '广东省深圳市福田区', 'finance', 0);
您是管理员,可以管理所有客户、报备和经销商信息。
您是经销商用户,可以管理客户和提交报备申请。