# 泊云智销通 CRM 系统 ## 项目简介 泊云智销通是一款面向企业渠道管理的 CRM 系统,核心解决**报备冲突**与**保护期归属**问题。系统通过**唯一有效报备机制**与**自动到期释放机制**,确保同一学校的同一项目在同一时间只能被一个经销商跟进,并在保护期结束后自动释放资源,提升渠道效率与公平性。 ### 核心价值 - **防撞单机制**:基于"学校+产品+项目类型"组合的唯一性校验,避免恶意抢占 - **保护期管理**:自动计算保护期,到期自动释放,无需人工干预 - **公平竞争**:防止资源长期占用,所有经销商公平竞争 - **全程可追溯**:报备记录永久保留,支持进展记录追踪 - **灵活配置**:支持手动输入学校名称,降低报备门槛 --- ## 技术架构 ### 后端技术栈 - **框架**: Spring Boot 2.7 - **ORM**: MyBatis - **数据库**: MySQL 8 - **安全**: Spring Security + JWT + BCrypt - **API 文档**: Swagger - **构建工具**: Maven - **定时任务**: Spring Task Scheduler ### 前端技术栈 - **框架**: Vue 3 + TypeScript - **UI 组件**: Element Plus - **路由**: Vue Router 4 - **状态管理**: Pinia - **HTTP 客户端**: Axios - **构建工具**: Vite - **代码规范**: ESLint + Prettier ### 架构模式 - **前后端分离**: RESTful API 架构 - **MVC 分层**: Controller → Service → Mapper → Entity - **数据库设计**: 遵循范式,合理使用索引 - **安全设计**: JWT 认证,BCrypt 密码加密 --- ## 用户角色与权限 ### 1. 管理员 **角色标识**: `role = 0` **权限范围**: - ✅ 报备管理(全部报备的查看、审核、编辑、作废) - ✅ 经销商管理(经销商的增删改查、重置密码) - ✅ 学校管理(查询、批量导入学校数据) - ✅ 系统配置(保护期天数等参数配置) - ✅ 查看所有运营数据 **主要职责**: - 审核报备申请 - 管理经销商账号 - 配置系统参数 - 监控渠道数据 ### 2. 经销商用户 **角色标识**: `role = 1` **权限范围**: - ✅ 查看自己的报备记录 - ✅ 提交报备申请 - ✅ 撤回待审核的报备 - ✅ 为已通过的报备添加进展记录 - ✅ 修改个人密码 **主要职责**: - 报备意向学校项目 - 跟进保护期内的项目 - 维护进展记录 ### 权限矩阵 | 功能模块 | 管理员 | 经销商 | |---------|--------|--------| | 报备管理 | 全部 | 自己的报备 | | 提交报备 | ✅ | ✅ | | 报备审核 | ✅ | ❌ | | 报备编辑/作废 | ✅ | ❌ | | 进展记录 | 全部 | 自己报备的进展 | | 经销商管理 | ✅ | ❌ | | 学校管理 | ✅ | ❌ | | 系统配置 | ✅ | ❌ | --- ## 功能清单 ### 1. 认证与授权 #### 1.1 用户登录 - **路径**: `POST /api/auth/login` - **描述**: 用户登录系统,获取 JWT Token - **安全**: BCrypt 加密存储 - **返回**: Token + 用户基本信息 **默认账号**: - 管理员: `admin` / `admin123` - 经销商用户: 根据经销商数据 #### 1.2 修改密码 - **路径**: `POST /api/auth/change-password` - **权限**: 所有用户 - **功能**: 用户修改自己的登录密码 - **验证**: 需要输入原密码验证 #### 1.3 重置密码 - **路径**: `POST /api/auth/reset-password` - **权限**: 仅管理员 - **功能**: 管理员重置经销商用户的密码 --- ### 2. 报备管理 #### 2.1 报备列表 - **路径**: `GET /api/report/page` - **权限**: 管理员查看全部,经销商查看自己的 - **功能**: 分页查询报备记录 - **搜索字段**: 学校名称、状态 - **状态说明**: - `0 - 待审核`: 等待管理员审核 - `1 - 已通过`: 审核通过,保护期内 - `2 - 已驳回`: 审核未通过 - `3 - 已失效`: 保护期已过 - `4 - 已作废`: 管理员主动作废 #### 2.2 提交报备 - **路径**: `POST /api/report` - **权限**: 经销商 - **功能**: 经销商报备意向学校项目 - **必填字段**: 学校名称、所属产品、项目类型 - **可选字段**: 报备说明 - **防撞单规则**: - 同一学校的同一产品+项目类型组合,只能有一个有效报备 - 有效报备定义:状态为"待审核(0)"或"已通过(1)" - 已失效、已驳回、已作废的报备不影响新报备 #### 2.3 撤回报备 - **路径**: `DELETE /api/report/{id}` - **权限**: 报备所属经销商 - **功能**: 撤回待审核的报备申请 - **限制**: 只能撤回状态为"待审核"的报备 #### 2.4 审核报备 - **路径**: `PUT /api/report/{id}/audit` - **权限**: 仅管理员 - **功能**: 审核报备申请 - **操作**: - 通过:设置保护期(默认90天),状态变为"已通过" - 驳回:填写驳回原因,状态变为"已驳回" #### 2.5 编辑报备 - **路径**: `PUT /api/report/{id}` - **权限**: 仅管理员 - **功能**: 编辑已通过的报备 - **可编辑字段**: 所属产品、项目类型、报备说明、状态 - **作废操作**: 状态改为"已作废"时必须填写作废原因 #### 2.6 报备详情 - **路径**: `GET /api/report/{id}` - **权限**: 根据权限查看 - **功能**: 查看报备详细信息和进展记录 --- ### 3. 进展记录管理 #### 3.1 添加进展记录 - **路径**: `POST /api/report-progress` - **权限**: 经销商只能为自己的报备添加,管理员可查看所有 - **功能**: 为已通过的报备添加进展记录 - **限制**: 只能为状态为"已通过"的报备添加进展 #### 3.2 编辑进展记录 - **路径**: `PUT /api/report-progress/{id}` - **权限**: 进展记录创建者 - **功能**: 修改进展记录内容 - **保留历史**: 不支持删除,确保数据完整性 --- ### 4. 经销商管理 #### 4.1 经销商列表 - **路径**: `GET /api/dealer/list` - **权限**: 仅管理员 - **功能**: 查询所有经销商信息 #### 4.2 新增经销商 - **路径**: `POST /api/dealer` - **权限**: 仅管理员 - **必填字段**: 经销商名称、经销商账号、联系人、联系电话 - **自动创建**: 创建经销商记录的同时创建对应的登录用户 #### 4.3 编辑经销商 - **路径**: `PUT /api/dealer/{id}` - **权限**: 仅管理员 - **功能**: 修改经销商信息 #### 4.4 删除经销商 - **路径**: `DELETE /api/dealer/{id}` - **权限**: 仅管理员 - **功能**: 删除经销商及其关联用户 --- ### 5. 学校管理 #### 5.1 学校列表 - **路径**: `GET /api/school/list` - **权限**: 所有用户 - **功能**: 查询所有学校信息 #### 5.2 学校搜索 - **路径**: `GET /api/school/search?keyword=xxx` - **权限**: 所有用户 - **功能**: 模糊搜索学校名称 - **应用场景**: 报备时学校名称的自动完成提示 #### 5.3 批量导入学校 - **路径**: `POST /api/school/import` - **权限**: 仅管理员 - **功能**: 上传 Excel 文件批量导入学校数据 - **文件格式**: `.xls` 或 `.xlsx` **学校数据结构**: - 学校标识码(唯一) - 学校名称 - 所在地(省份 + 城市) --- ### 6. 系统配置 #### 6.1 保护期配置 - **配置项**: `report.protect.days` - **默认值**: `90` 天 - **说明**: 控制报备通过后的保护期时长 #### 6.2 报备冲突控制 - **配置项**: `report.allow.overlap` - **默认值**: `false` - **说明**: 是否允许重叠报备(仅用于测试) - **生产环境**: 必须为 `false` --- ## 业务流程 ### 报备完整生命周期 ``` ┌─────────────────────────────────────────────────────────────┐ │ 报备完整流程 │ └─────────────────────────────────────────────────────────────┘ 1. 经销商提交报备 ├─ 学校名称(可从学校库选择或手动输入) ├─ 所属产品(必填) ├─ 项目类型(必填) ├─ 报备说明(可选) └─ 提交审核 ↓ 状态:待审核(0) 2. 管理员审核 ├─ 通过 → 设置保护期(默认90天) │ ├─ 报备状态:已通过(1) │ └─ 保护期倒计时开始 │ └─ 驳回 → 填写驳回原因 └─ 报备状态:已驳回(2) 3. 保护期内 ├─ 该"学校+产品+项目类型"组合被锁定 ├─ 其他经销商无法报备相同组合 ├─ 经销商可添加进展记录 └─ 剩余保护天数倒计时 4. 管理员操作(可选) ├─ 编辑报备:修改产品、项目类型、说明 └─ 作废报备:状态改为已作废(4),填写原因 └─ 作废后该组合可重新报备 5. 保护期到期(定时任务) └─ 每天凌晨1点自动执行 ├─ 报备状态:已通过 → 已失效(3) └─ 该"学校+产品+项目类型"组合可再次报备 ``` ### 报备状态转换规则 ``` 待审核(0) ↓ [管理员审核通过] 已通过(1) ←───────┐ ↓ │ │ [保护期到期] │ [管理员编辑恢复] ↓ │ 已失效(3) ─────────┘ 待审核(0) ↓ [管理员驳回] 已驳回(2) ←─── 终态 待审核(0) ↓ [经销商撤回] 删除记录 ←─── 物理删除 已通过(1) ↓ [管理员作废] 已作废(4) ←─── 终态 ``` --- ## 数据库设计 ### 核心表结构 #### 1. 报备表 (crm_report) | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 报备ID(主键) | | dealer_id | BIGINT | 经销商ID | | school_id | BIGINT | 学校ID(可为NULL,手动输入时为空) | | school_name | VARCHAR(255) | 学校名称(冗余存储,方便查询) | | product | VARCHAR(255) | 所属产品 | | project_type | VARCHAR(255) | 项目类型 | | description | VARCHAR(500) | 报备说明 | | status | TINYINT | 状态:0-待审核 1-已通过 2-已驳回 3-已失效 4-已作废 | | reject_reason | VARCHAR(255) | 驳回原因 | | cancel_reason | VARCHAR(500) | 作废原因 | | protect_start_date | DATE | 保护期开始日期 | | protect_end_date | DATE | 保护期结束日期 | | created_at | DATETIME | 创建时间 | | updated_at | DATETIME | 更新时间 | **索引**: - `idx_dealer_id` - 经销商ID索引 - `idx_status` - 状态索引 - `idx_protect_end_date` - 保护期结束日期索引(定时任务使用) #### 2. 经销商表 (crm_dealer) | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 经销商ID(主键) | | name | VARCHAR(200) | 经销商名称 | | code | VARCHAR(50) | 经销商账号(唯一) | | contact_person | VARCHAR(50) | 联系人 | | contact_phone | VARCHAR(20) | 联系电话 | | email | VARCHAR(100) | 邮箱 | | status | TINYINT | 状态:0-禁用 1-启用 | | created_at | DATETIME | 创建时间 | | updated_at | DATETIME | 更新时间 | #### 3. 用户表 (crm_user) | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 用户ID(主键) | | username | VARCHAR(50) | 用户名(登录账号) | | password | VARCHAR(255) | 密码(BCrypt加密) | | dealer_id | BIGINT | 关联经销商ID(管理员为NULL) | | role | TINYINT | 角色:0-管理员 1-经销商用户 | | status | TINYINT | 状态:0-禁用 1-启用 | | created_at | DATETIME | 创建时间 | | updated_at | DATETIME | 更新时间 | #### 4. 学校表 (crm_school) | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 学校ID(主键) | | school_code | VARCHAR(50) | 学校标识码(唯一) | | school_name | VARCHAR(200) | 学校名称 | | location | VARCHAR(255) | 所在地 | | created_at | DATETIME | 创建时间 | | updated_at | DATETIME | 更新时间 | **索引**: - `uk_school_code` - 学校标识码唯一索引 - `idx_school_name` - 学校名称索引 #### 5. 报备进展记录表 (crm_report_progress) | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 主键ID | | report_id | BIGINT | 报备ID | | progress_content | VARCHAR(500) | 进展内容 | | created_by | BIGINT | 创建人ID | | created_at | DATETIME | 创建时间 | | updated_at | DATETIME | 更新时间 | **索引**: - `idx_report_id` - 报备ID索引 - `idx_created_by` - 创建人索引 #### 6. 系统配置表 (crm_system_config) | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 主键ID | | config_key | VARCHAR(100) | 配置键(唯一) | | config_value | VARCHAR(500) | 配置值 | | config_type | VARCHAR(20) | 配置类型(string/integer/boolean) | | is_editable | TINYINT | 是否可编辑(1-是 0-否) | | created_at | DATETIME | 创建时间 | | updated_at | DATETIME | 更新时间 | --- ## 定时任务 ### 报备过期处理 **执行时间**: 每天凌晨 1 点 **Cron 表达式**: `0 0 1 * * ?` **功能**: 1. 查询所有保护期已到的报备(`protect_end_date <= 今天` 且 `status = 1`) 2. 批量更新报备状态为"已失效"(`status = 3`) 3. 释放资源,允许相同"学校+产品+项目类型"组合重新报备 **核心代码**: `ReportExpireTask.java` ```java @Scheduled(cron = "0 0 1 * * ?") public void handleExpiredReports() { log.info("开始处理过期报备..."); try { reportService.handleExpiredReports(); log.info("过期报备处理完成"); } catch (Exception e) { log.error("处理过期报备失败", e); } } ``` --- ## 防撞单机制详解 ### 核心规则 **唯一性维度**: 学校 + 产品 + 项目类型 **有效报备定义**: 状态为"待审核(0)"或"已通过(1)"的报备 ### 校验逻辑 ```java // 1. 如果选择了学校(有 school_id) // 按 school_id + product + project_type 精确匹配 WHERE school_id = #{schoolId} AND product = #{product} AND project_type = #{projectType} AND status IN (0, 1) // 2. 如果手动输入学校(无 school_id) // 按 school_name + product + project_type 匹配 WHERE school_name = #{schoolName} AND product = #{product} AND project_type = #{projectType} AND status IN (0, 1) ``` ### 业务意义 - **防止撞单**: 同一项目不能被多个经销商同时报备 - **公平竞争**: 保护期内锁定,到期后自动释放 - **灵活性**: 支持手动输入学校名称,降低报备门槛 --- ## 学校选择机制 ### 学校数据来源 学校数据存储在 `crm_school` 表中,包含: - 学校标识码(唯一) - 学校名称 - 所在地 ### 前端交互 1. **自动完成搜索** - 用户输入学校名称 - 前端调用 `/api/school/search?keyword=xxx` 接口 - 后端模糊匹配 `school_name` - 返回匹配列表供用户选择 2. **手动输入** - 用户可以直接输入学校名称 - 如果不从下拉列表选择,`school_id` 为 NULL - `school_name` 保存用户输入的文本 - 防撞单校验时按 `school_name` 进行匹配 ### 设计优势 - **降低门槛**: 学校不在库中也能报备,不影响业务 - **数据规范化**: 优先使用学校库数据,保证数据质量 - **灵活扩展**: 支持批量导入学校数据 --- ## 安全机制 ### 1. 认证与授权 #### JWT Token 认证 - 用户登录后获取 JWT Token - Token 包含:用户ID、用户名、角色、经销商ID - Token 存储在客户端 `localStorage` - 每次请求在 Header 中携带:`Authorization: Bearer {token}` #### 权限拦截 - 前端路由守卫:未登录重定向到登录页 - 后端拦截器:验证 Token 有效性 - 数据权限:经销商只能查看自己的报备 ### 2. 密码安全 #### 密码规则 - 长度:6-20 位 - 加密:BCrypt(自动加盐) - 修改密码:需验证原密码 --- ## 项目结构 ``` by-crm/ ├── backend/ # 后端工程 │ ├── src/main/java/com/bycrm/ │ │ ├── annotations/ # 自定义注解(权限等) │ │ ├── common/ # 公共类(Result、Constants等) │ │ ├── config/ # 配置类(CORS、MyBatis、Swagger等) │ │ ├── controller/ # 控制器层 │ │ │ ├── AuthController.java │ │ │ ├── ReportController.java │ │ │ ├── DealerController.java │ │ │ ├── SchoolController.java │ │ │ └── ReportProgressController.java │ │ ├── dto/ # 数据传输对象 │ │ ├── entity/ # 实体类 │ │ ├── exception/ # 异常处理 │ │ ├── mapper/ # MyBatis Mapper 接口 │ │ ├── service/ # 业务逻辑层 │ │ ├── task/ # 定时任务 │ │ ├── util/ # 工具类(JWT、加密等) │ │ ├── vo/ # 视图对象 │ │ └── ByCrmApplication.java │ ├── src/main/resources/ │ │ ├── mapper/*.xml # MyBatis SQL 映射 │ │ └── application.yml # 配置文件 │ └── pom.xml # Maven 依赖 │ ├── frontend/ # 前端工程 │ ├── src/ │ │ ├── api/ # API 接口封装 │ │ ├── assets/ # 静态资源 │ │ ├── components/ # 通用组件 │ │ ├── router/ # Vue Router 配置 │ │ ├── stores/ # Pinia 状态管理 │ │ ├── types/ # TypeScript 类型定义 │ │ ├── utils/ # 工具函数(请求、加密等) │ │ ├── views/ # 页面组件 │ │ │ ├── Login.vue # 登录页 │ │ │ ├── Layout.vue # 主布局 │ │ │ ├── Dashboard.vue # 首页 │ │ │ ├── Report.vue # 报备管理 │ │ │ └── Dealer.vue # 经销商管理 │ │ ├── App.vue # 根组件 │ │ └── main.ts # 入口文件 │ ├── vite.config.ts # Vite 配置 │ ├── package.json # 依赖配置 │ └── tsconfig.json # TS 配置 │ ├── docs/ # 文档目录 │ └── README.md # 项目文档 │ └── sql/ # 数据库脚本 ├── init.sql # 初始化脚本 └── *.sql # 其他 SQL 脚本 ``` --- ## 环境配置 ### 后端配置 (application.yml) ```yaml server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/by_crm username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.bycrm.entity # JWT 配置 jwt: secret: your-secret-key expiration: 86400000 # 24小时(毫秒) # 保护期配置 crm: report: protect-days: 90 # 保护期天数(默认90天) allow-overlap: false # 是否允许报备重叠(测试用,生产必须false) ``` ### 前端配置 ```typescript // API 基础地址 const BASE_URL = 'http://localhost:8080/api' // Token 存储 const TOKEN_KEY = 'token' ``` --- ## 部署说明 ### 开发环境启动 **后端**: ```bash cd backend mvn spring-boot:run # 访问: http://localhost:8080 # Swagger: http://localhost:8080/swagger-ui.html ``` **前端**: ```bash cd frontend pnpm install pnpm dev # 访问: http://localhost:5173 ``` ### 生产环境部署 **后端**: ```bash # 编译打包 mvn clean package # 运行 JAR java -jar target/bycrm-0.0.1-SNAPSHOT.jar ``` **前端**: ```bash # 编译打包 pnpm build # 部署 dist 目录到 Web 服务器 ``` ### 数据库初始化 ```bash # 创建数据库 mysql -u root -p -e "CREATE DATABASE by_crm CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci" # 导入初始化脚本 mysql -u root -p by_crm < sql/init.sql ``` --- ## API 接口清单 ### 认证接口 - `POST /api/auth/login` - 用户登录 - `GET /api/auth/user/info` - 获取当前用户信息 - `POST /api/auth/logout` - 用户退出 - `POST /api/auth/change-password` - 修改密码 - `POST /api/auth/reset-password` - 重置密码 ### 报备接口 - `GET /api/report/page` - 分页查询报备 - `GET /api/report/{id}` - 获取报备详情 - `POST /api/report` - 创建报备 - `PUT /api/report/{id}` - 更新报备 - `DELETE /api/report/{id}` - 撤回报备 - `PUT /api/report/{id}/audit` - 审核报备 ### 进展记录接口 - `GET /api/report-progress/list?reportId={id}` - 查询进展记录 - `POST /api/report-progress` - 添加进展记录 - `PUT /api/report-progress/{id}` - 编辑进展记录 ### 经销商接口(仅管理员) - `GET /api/dealer/list` - 查询所有经销商 - `POST /api/dealer` - 创建经销商 - `PUT /api/dealer/{id}` - 更新经销商 - `DELETE /api/dealer/{id}` - 删除经销商 ### 学校接口 - `GET /api/school/list` - 查询所有学校 - `GET /api/school/search?keyword=xxx` - 搜索学校 - `POST /api/school` - 创建学校 - `POST /api/school/import` - 文件上传导入 --- ## 版本历史 ### v1.0.0 (当前版本) **核心功能**: - ✅ 报备管理(提交、审核、撤回、编辑、作废) - ✅ 经销商管理(增删改查、重置密码) - ✅ 学校管理(查询、搜索、导入) - ✅ 用户认证(登录、密码管理) - ✅ 进展记录(添加、编辑、查询) - ✅ 自动保护期到期处理 - ✅ 防撞单机制(学校+产品+项目类型) - ✅ 灵活的学校输入(库选择+手动输入) **安全特性**: - ✅ JWT Token 认证 - ✅ BCrypt 密码加密存储 - ✅ 权限注解拦截 - ✅ SQL 注入防护 - ✅ XSS 防护 --- ## 常见问题 ### 1. 默认账号是什么? - 管理员账号:`admin` / `admin123` - 经销商账号:根据经销商数据创建 ### 2. 保护期可以修改吗? - 可以,通过修改系统配置 `report.protect.days` - 默认为 90 天,可根据业务需求调整 ### 3. 同一学校项目可以同时被多个经销商报备吗? - **不可以**,这是系统的核心规则 - 同一"学校+产品+项目类型"组合只能有一个"有效"报备 - 有效报备定义:状态为"待审核(0)"或"已通过(1)" ### 4. 保护期到期后会发生什么? - 定时任务每天凌晨1点自动执行 - 将过期报备状态改为"已失效" - 该"学校+产品+项目类型"组合可重新报备 ### 5. 学校名称可以手动输入吗? - 可以,支持从学校库选择或手动输入 - 从学校库选择时会保存 `school_id` 和标准学校名称 - 手动输入时 `school_id` 为 NULL,保存用户输入的文本 ### 6. 报备可以作废吗? - 可以,只有管理员可以作废已通过的报备 - 作废时必须填写作废原因 - 作废后该"学校+产品+项目类型"组合可重新报备 --- ## 技术支持 ### 开发团队 - 后端开发:Spring Boot + MyBatis + MySQL - 前端开发:Vue 3 + TypeScript + Element Plus - 项目管理:Maven + pnpm ### 联系方式 - 问题反馈:提交 Issue 到项目仓库 - 技术文档:详见 `docs/` 目录 --- **版权声明**: 本系统为企业内部使用,未经授权不得复制或传播。