diff --git a/.DS_Store b/.DS_Store index a2f3500c..4c6310ab 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/docs/business-schema-design.md b/docs/business-schema-design.md deleted file mode 100644 index 8b1e5b43..00000000 --- a/docs/business-schema-design.md +++ /dev/null @@ -1,526 +0,0 @@ -# CRM业务表设计 - -## 1. 设计依据 - -本设计根据当前前端页面反推业务模型,涉及页面如下: - -- `src/pages/Opportunities.tsx`:商机储备、商机详情、跟进记录 -- `src/pages/Expansion.tsx`:销售人员拓展、渠道拓展、跟进记录 -- `src/pages/Work.tsx`:外勤打卡、日报、历史记录、主管点评 -- `src/pages/Profile.tsx`:个人资料、统计信息 -- `src/pages/Dashboard.tsx`:首页统计、待办、动态 - -当前前端是展示型页面,未接入真实接口,因此以下设计属于“按现有前端信息推导出的第一版业务库设计”。 - ---- - -## 2. 业务模块拆分 - -建议按以下模块建表: - -1. 组织与人员 -2. 客户与商机 -3. 拓展管理 -4. 工作管理 -5. 待办与动态(可选增强) - ---- - -## 3. 核心实体关系 - -```text -部门 department - └─< 用户 user - -客户 customer - └─< 商机 opportunity - └─< 商机跟进记录 opportunity_followup - -用户 user - └─< 销售拓展 sales_expansion - └─< 拓展跟进记录 expansion_followup - -用户 user - └─< 渠道拓展 channel_expansion - └─< 拓展跟进记录 expansion_followup - -用户 user - └─< 外勤打卡 work_checkin - └─< 打卡附件 work_checkin_attachment - -用户 user - └─< 日报 work_daily_report - └─< 日报点评 work_daily_report_comment -``` - ---- - -## 4. 表设计 - -## 4.1 `sys_department` 部门表 - -用于承接“华东大区、华北大区”等组织信息。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| dept_code | varchar(50) | 部门编码 | -| dept_name | varchar(100) | 部门名称 | -| parent_id | bigint | 上级部门ID | -| manager_user_id | bigint | 部门负责人 | -| status | tinyint | 1启用 0停用 | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - ---- - -## 4.2 `sys_user` 用户表 - -承接“张三、李四、王五”等销售人员,以及个人页中的员工资料。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| user_code | varchar(50) | 工号/员工编号 | -| username | varchar(50) | 登录账号 | -| real_name | varchar(50) | 姓名 | -| mobile | varchar(20) | 手机号 | -| email | varchar(100) | 邮箱 | -| dept_id | bigint | 所属部门 | -| job_title | varchar(100) | 职位,如高级销售 | -| status | tinyint | 1在职 0离职 | -| hire_date | date | 入职日期 | -| avatar_url | varchar(255) | 头像地址 | -| password_hash | varchar(255) | 登录密码摘要 | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - -索引建议: - -- `idx_user_dept_id(dept_id)` -- `uk_user_username(username)` -- `uk_user_mobile(mobile)` - ---- - -## 4.3 `crm_customer` 客户表 - -来源于商机页面中的“客户名称”,如医院、学校、集团。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| customer_code | varchar(50) | 客户编码 | -| customer_name | varchar(200) | 客户名称 | -| customer_type | varchar(50) | 客户类型,如医院/高校/企业 | -| industry | varchar(50) | 行业 | -| province | varchar(50) | 省份 | -| city | varchar(50) | 城市 | -| address | varchar(255) | 地址 | -| owner_user_id | bigint | 当前负责人 | -| source | varchar(50) | 来源,如渠道推荐/市场活动 | -| status | varchar(30) | 潜在/跟进中/成交/流失 | -| remark | text | 备注 | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - -索引建议: - -- `idx_customer_owner(owner_user_id)` -- `idx_customer_name(customer_name)` - ---- - -## 4.4 `crm_opportunity` 商机表 - -这是前端最核心业务表,对应“商机储备”页面。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| opportunity_code | varchar(50) | 商机编号,如 `HD-20231024-001` | -| opportunity_name | varchar(200) | 商机名称 | -| customer_id | bigint | 关联客户 | -| owner_user_id | bigint | 商机负责人 | -| amount | decimal(18,2) | 商机金额 | -| expected_close_date | date | 预计结单日期 | -| confidence_pct | tinyint | 把握度,0-100 | -| stage | varchar(50) | 阶段,如初步沟通/方案交流/招投标/商务谈判/已成交 | -| opportunity_type | varchar(50) | 类型,如新建/扩容 | -| product_type | varchar(100) | 产品类别,如VDI/VOI/IDV云桌面 | -| source | varchar(50) | 商机来源 | -| pushed_to_oms | tinyint | 是否已推送 OMS | -| oms_push_time | datetime | 推送 OMS 时间 | -| description | text | 商机说明/备注 | -| status | varchar(30) | 正常/赢单/输单/关闭 | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - -索引建议: - -- `uk_opportunity_code(opportunity_code)` -- `idx_opportunity_customer(customer_id)` -- `idx_opportunity_owner(owner_user_id)` -- `idx_opportunity_stage(stage)` -- `idx_opportunity_expected_close(expected_close_date)` - ---- - -## 4.5 `crm_opportunity_followup` 商机跟进记录表 - -对应商机详情里的“跟进记录”时间线。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| opportunity_id | bigint | 商机ID | -| followup_time | datetime | 跟进时间 | -| followup_type | varchar(50) | 跟进方式,如电话沟通/现场拜访/微信沟通 | -| content | text | 跟进内容 | -| next_action | varchar(255) | 下一步动作 | -| followup_user_id | bigint | 跟进人 | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - -索引建议: - -- `idx_opp_followup_opportunity(opportunity_id, followup_time desc)` -- `idx_opp_followup_user(followup_user_id)` - ---- - -## 4.6 `crm_sales_expansion` 销售拓展表 - -对应“销售人员拓展”。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| candidate_name | varchar(50) | 候选人姓名 | -| mobile | varchar(20) | 手机号 | -| email | varchar(100) | 邮箱 | -| target_dept_id | bigint | 目标归属部门 | -| industry | varchar(50) | 擅长行业 | -| title | varchar(100) | 当前或目标职位 | -| intent_level | varchar(20) | 意向度,高/中/低 | -| stage | varchar(50) | 阶段,如初步沟通/方案交流 | -| has_desktop_exp | tinyint | 是否有云桌面经验 | -| in_progress | tinyint | 是否持续跟进中 | -| employment_status | varchar(20) | 在职/离职/已入职/已放弃 | -| expected_join_date | date | 预计入职日期,可为空 | -| owner_user_id | bigint | 负责人 | -| remark | text | 备注 | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - -索引建议: - -- `idx_sales_expansion_owner(owner_user_id)` -- `idx_sales_expansion_stage(stage)` -- `idx_sales_expansion_mobile(mobile)` - ---- - -## 4.7 `crm_channel_expansion` 渠道拓展表 - -对应“渠道拓展”。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| channel_name | varchar(200) | 渠道名称 | -| province | varchar(50) | 所在省份 | -| industry | varchar(50) | 主要行业 | -| annual_revenue | decimal(18,2) | 年营收规模 | -| staff_size | int | 公司人数 | -| contact_name | varchar(50) | 联系人 | -| contact_title | varchar(100) | 联系人职务 | -| contact_mobile | varchar(20) | 联系电话 | -| stage | varchar(50) | 阶段,如初步接触/合作洽谈 | -| landed_flag | tinyint | 是否已落地 | -| expected_sign_date | date | 预计签约日期 | -| owner_user_id | bigint | 负责人 | -| remark | text | 备注 | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - -索引建议: - -- `idx_channel_expansion_owner(owner_user_id)` -- `idx_channel_expansion_stage(stage)` -- `idx_channel_expansion_name(channel_name)` - ---- - -## 4.8 `crm_expansion_followup` 拓展跟进记录表 - -销售拓展和渠道拓展都存在“跟进记录”,建议共用一张表,通过对象类型区分。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| biz_type | varchar(20) | `sales` / `channel` | -| biz_id | bigint | 对应拓展对象ID | -| followup_time | datetime | 跟进时间 | -| followup_type | varchar(50) | 电话/微信/面谈等 | -| content | text | 跟进内容 | -| next_action | varchar(255) | 下一步动作 | -| followup_user_id | bigint | 跟进人 | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - -索引建议: - -- `idx_exp_followup_biz(biz_type, biz_id, followup_time desc)` -- `idx_exp_followup_user(followup_user_id)` - ---- - -## 4.9 `work_checkin` 外勤打卡表 - -对应工作台里的“外勤打卡”。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| user_id | bigint | 打卡人 | -| checkin_date | date | 打卡日期 | -| checkin_time | datetime | 打卡时间 | -| longitude | decimal(10,6) | 经度 | -| latitude | decimal(10,6) | 纬度 | -| location_text | varchar(255) | 地址文本 | -| remark | varchar(500) | 备注说明 | -| status | varchar(30) | 正常/异常/补卡 | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - -索引建议: - -- `idx_checkin_user_date(user_id, checkin_date desc)` - ---- - -## 4.10 `work_checkin_attachment` 打卡附件表 - -前端要求“现场照片必填”,建议单独拆附件表。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| checkin_id | bigint | 打卡ID | -| file_url | varchar(255) | 文件地址 | -| file_type | varchar(30) | image/audio/video | -| file_name | varchar(255) | 原始文件名 | -| file_size | bigint | 文件大小 | -| created_at | datetime | 创建时间 | - -索引建议: - -- `idx_checkin_attachment_checkin(checkin_id)` - ---- - -## 4.11 `work_daily_report` 日报表 - -对应“每日表”和右侧历史日报。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| user_id | bigint | 提交人 | -| report_date | date | 日报日期 | -| work_content | text | 今日工作内容 | -| tomorrow_plan | text | 明日工作计划 | -| source_type | varchar(30) | manual/voice | -| submit_time | datetime | 提交时间 | -| status | varchar(30) | 待提交/已提交/已阅/已点评 | -| score | int | 评分 | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - -索引建议: - -- `idx_daily_report_user_date(user_id, report_date desc)` -- `idx_daily_report_status(status)` -- `uk_daily_report_user_date(user_id, report_date)` - ---- - -## 4.12 `work_daily_report_comment` 日报点评表 - -对应历史记录中的“主管点评”。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| report_id | bigint | 日报ID | -| reviewer_user_id | bigint | 点评人 | -| score | int | 评分 | -| comment_content | text | 点评内容 | -| reviewed_at | datetime | 点评时间 | -| created_at | datetime | 创建时间 | - -索引建议: - -- `idx_report_comment_report(report_id)` - ---- - -## 4.13 `work_todo` 待办表(建议补充) - -首页有“待办事项”,虽然现在是静态数据,但真实业务通常需要。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| user_id | bigint | 所属用户 | -| title | varchar(200) | 待办标题 | -| biz_type | varchar(30) | opportunity / expansion / report / other | -| biz_id | bigint | 业务对象ID | -| due_date | datetime | 截止时间 | -| status | varchar(20) | todo / done / canceled | -| priority | varchar(20) | high / medium / low | -| created_at | datetime | 创建时间 | -| updated_at | datetime | 更新时间 | - ---- - -## 4.14 `sys_activity_log` 动态日志表(建议补充) - -首页“最新动态”适合由统一动态表驱动。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint PK | 主键 | -| biz_type | varchar(30) | 业务类型 | -| biz_id | bigint | 业务ID | -| action_type | varchar(50) | 如商机阶段更新/日报点评/新增渠道 | -| title | varchar(200) | 动态标题 | -| content | varchar(500) | 动态描述 | -| operator_user_id | bigint | 操作人 | -| created_at | datetime | 创建时间 | - -索引建议: - -- `idx_activity_created(created_at desc)` -- `idx_activity_biz(biz_type, biz_id)` - ---- - -## 5. 推荐枚举值 - -为了避免硬编码中文状态,建议状态字段统一使用编码值,前端再做字典映射。 - -### 商机阶段 `opportunity.stage` - -- `initial_contact` 初步沟通 -- `solution_discussion` 方案交流 -- `bidding` 招投标 -- `business_negotiation` 商务谈判 -- `won` 已成交 -- `lost` 已丢单 - -### 商机状态 `opportunity.status` - -- `active` -- `won` -- `lost` -- `closed` - -### 销售拓展意向 `sales_expansion.intent_level` - -- `high` -- `medium` -- `low` - -### 外勤打卡状态 `work_checkin.status` - -- `normal` -- `abnormal` -- `reissue` - -### 日报状态 `work_daily_report.status` - -- `draft` -- `submitted` -- `read` -- `reviewed` - ---- - -## 6. 第一版最小可落地表 - -如果先做 MVP,建议优先落以下 8 张: - -1. `sys_department` -2. `sys_user` -3. `crm_customer` -4. `crm_opportunity` -5. `crm_opportunity_followup` -6. `crm_channel_expansion` -7. `work_checkin` -8. `work_daily_report` - -这 8 张就能支撑目前前端大部分核心展示。 - -如果要把“拓展管理”做完整,再补: - -1. `crm_sales_expansion` -2. `crm_expansion_followup` -3. `work_checkin_attachment` -4. `work_daily_report_comment` - ---- - -## 7. 建表原则建议 - -1. 所有业务表统一保留 `created_at`、`updated_at`。 -2. 负责人、创建人、点评人等人员字段统一关联 `sys_user.id`。 -3. 跟进记录建议独立建表,不要直接塞进主表。 -4. 金额统一用 `decimal(18,2)`,不要用字符串。 -5. 时间类字段区分清楚 `date` 和 `datetime`。 -6. 中文状态不要直接写死在数据库,建议存编码值。 -7. 如果后续要对接 OMS,商机表中保留 `pushed_to_oms` 和外部系统单号字段会更稳妥。 - ---- - -## 8. 与前端页面的映射关系 - -### 商机页 - -- 列表:`crm_opportunity` -- 客户名称:`crm_customer` -- 详情跟进:`crm_opportunity_followup` - -### 拓展页 - -- 销售拓展:`crm_sales_expansion` -- 渠道拓展:`crm_channel_expansion` -- 跟进记录:`crm_expansion_followup` - -### 工作页 - -- 外勤打卡:`work_checkin` -- 打卡照片:`work_checkin_attachment` -- 日报:`work_daily_report` -- 主管点评:`work_daily_report_comment` - -### 首页/我的 - -- 统计看板:由商机、拓展、打卡、日报聚合生成 -- 待办事项:`work_todo` -- 最新动态:`sys_activity_log` -- 个人信息:`sys_user` + `sys_department` - ---- - -## 9. 下一步建议 - -如果要继续往后端落地,建议按以下顺序推进: - -1. 先定字段字典和状态流转 -2. 再输出 MySQL DDL -3. 再补接口文档 -4. 最后和前端页面一一对齐新增/编辑/详情接口 - -如果需要,我下一步可以直接继续帮你补一版 MySQL 建表 SQL。 diff --git a/docs/crm.md b/docs/crm.md new file mode 100644 index 00000000..84f7c754 --- /dev/null +++ b/docs/crm.md @@ -0,0 +1,641 @@ +# 紫光汇智 CRM 技术系统规划与技术文档 + +## 1. 文档定位 + +本文档面向研发、架构、测试、运维与交付团队,说明紫光汇智 CRM 系统的技术现状、目标架构、模块规划、关键设计原则、实施路线与交付边界。 + +文档目标如下: + +- 为当前系统提供统一的技术说明基线 +- 为后续迭代提供可落地的技术规划 +- 为环境部署、模块扩展、接口治理和性能治理提供参考 +- 为研发协作、测试设计和运维交接提供统一口径 + +本文档基于当前仓库代码、SQL 初始化脚本与现有项目文档整理,整理日期为 `2026-04-03`。 + +## 2. 系统概述 + +紫光汇智 CRM 是一个围绕销售过程管理建设的轻量 CRM 系统,当前主要覆盖以下业务域: + +- 首页工作台聚合 +- 销售拓展与渠道拓展 +- 商机储备与推进 +- 外勤打卡 +- 销售日报 +- 个人资料与账号安全 + +系统当前采用前后端分离模式: + +- 前端:`frontend/`,基于 `Vite + React + TypeScript` +- 后端:`backend/`,基于 `Spring Boot + MyBatis Plus + PostgreSQL + Redis` +- 数据:通过 `sql/init_full_pg17.sql` 初始化业务表 + +## 3. 当前技术现状 + +### 3.1 前端现状 + +前端目录位于 `frontend/src/`,核心结构如下: + +```text +frontend/src/ +├── App.tsx +├── components/ +├── hooks/ +├── lib/ +└── pages/ +``` + +当前页面模块包括: + +- `Login.tsx`:登录页 +- `Dashboard.tsx`:首页工作台 +- `Expansion.tsx`:销售拓展、渠道拓展 +- `Opportunities.tsx`:商机管理 +- `Work.tsx`:打卡、日报、历史记录 +- `Profile.tsx`:个人中心 + +当前前端特点: + +- 使用 `react-router-dom` 管理路由 +- 通过 `lib/auth.ts` 封装统一 API 调用 +- 已适配 PC 与移动端 +- 页面交互完整,具备较强的可交付性 +- 复杂页面采用单文件实现,后续存在进一步拆分空间 + +### 3.2 后端现状 + +后端目录位于 `backend/src/main/java/com/unis/crm/`,当前结构包括: + +```text +backend/src/main/java/com/unis/crm/ +├── common/ +├── config/ +├── controller/ +├── dto/ +├── mapper/ +├── service/ +└── service/impl/ +``` + +当前已实现的控制器包括: + +- `DashboardController` +- `ExpansionController` +- `OpportunityController` +- `WorkController` +- `ProfileController` +- `OpportunityIntegrationController` +- `WecomSsoController` + +当前后端特点: + +- 采用 Spring Boot 单体模式 +- 基于 MyBatis Plus 组织数据访问 +- DTO 分包清晰,按业务域拆分 +- 已具备基础异常处理、统一响应和安全配置 +- 业务仍集中在服务实现层,后续可继续增强领域边界和可测试性 + +### 3.3 数据层现状 + +当前项目已将业务初始化入口收敛为: + +- `sql/init_full_pg17.sql` + +脚本中已覆盖以下核心表: + +- `crm_customer` +- `crm_opportunity` +- `crm_opportunity_followup` +- `crm_sales_expansion` +- `crm_channel_expansion` +- `crm_channel_expansion_contact` +- `crm_expansion_followup` +- `work_checkin` +- `work_daily_report` +- `work_daily_report_comment` +- `work_todo` +- `sys_activity_log` + +当前数据层特点: + +- 业务主表与过程表已较完整 +- 初始化脚本已能作为统一部署入口 +- 仍依赖基础平台表与基础数据 +- 后续需继续收敛索引策略、归档策略和审计策略 + +## 4. 技术建设目标 + +未来技术建设建议围绕以下五个目标推进: + +1. 稳定性目标:保证主链路功能可持续发布与回归验证。 +2. 可维护性目标:降低单文件复杂度,提升模块边界清晰度。 +3. 可扩展性目标:为后续客户管理、审批流、分析报表等能力预留扩展空间。 +4. 可运维性目标:完善日志、监控、配置治理和部署标准。 +5. 安全性目标:提升鉴权、配置、上传、接口访问和数据变更可控性。 + +## 5. 总体技术架构规划 + +建议系统继续保持“前后端分离 + 单体业务后端”的建设模式,在当前阶段不急于拆分微服务,以降低复杂度与运维成本。 + +推荐总体架构如下: + +```mermaid +flowchart TD + U["用户端
PC / Mobile"] --> FE["前端应用
React + Vite"] + FE --> GW["应用接入层
Nginx / HTTPS"] + GW --> BE["CRM 后端单体服务
Spring Boot"] + BE --> PG["PostgreSQL"] + BE --> RD["Redis"] + BE --> BASE["基础平台能力
用户/组织/参数/鉴权"] + BE --> OMS["外部系统集成
OMS / 其他业务系统"] + BE --> FILE["文件存储目录
上传附件 / 打卡照片"] +``` + +规划原则: + +- 业务复杂度未达到微服务拆分必要性前,优先优化单体结构 +- 前端优先按业务域拆分组件和状态 +- 后端优先按业务模块做清晰分层,而不是过早做技术拆分 +- 数据库优先保障一致性与查询性能 +- 接口治理、日志治理、异常治理要优先于功能外延扩张 + +## 6. 模块规划 + +### 6.1 首页工作台模块 + +职责: + +- 聚合展示欢迎语、统计指标、待办事项、最新动态 +- 提供工作入口跳转 + +技术规划: + +- 统计接口与待办接口继续保留聚合型接口风格 +- 增加缓存策略,减少重复统计计算 +- 动态与待办建议做统一事件源或统一日志源治理 + +### 6.2 拓展管理模块 + +职责: + +- 管理销售拓展与渠道拓展主数据 +- 支撑后续商机关联、日报关联和项目推进 + +技术规划: + +- 拓展列表查询支持分页、筛选、导出、关键词搜索 +- 跟进记录数据结构统一化 +- 重复校验规则集中到服务层或领域规则层 +- 前端表单组件逐步抽象,降低 `Expansion.tsx` 单文件复杂度 + +### 6.3 商机管理模块 + +职责: + +- 管理商机主数据、推进阶段、竞争态势、售前协同和系统推送 + +技术规划: + +- 商机状态机建议标准化 +- 推送外部系统逻辑与本地业务保存逻辑解耦 +- 推送记录、失败原因、重试机制单独建模 +- 回写字段、自动跟进字段需进一步规范来源 + +### 6.4 工作管理模块 + +职责: + +- 支撑打卡、日报、历史记录和过程数据留痕 + +技术规划: + +- 打卡上传、定位、逆地理解析可进一步隔离为基础服务组件 +- 日报行项目结构建议形成稳定 DTO 契约 +- 历史记录建议增加分页游标和时间维度索引优化 +- 主管点评能力后续可进一步显式建模为评审子域 + +### 6.5 个人中心模块 + +职责: + +- 查看与修改个人资料 +- 密码修改 +- 展示个人月度工作概览 + +技术规划: + +- 用户资料更新与基础平台用户数据同步策略需要明确 +- 密码修改链路需与统一安全策略对齐 + +## 7. 分层设计规划 + +建议后端逐步收敛为以下分层: + +### 7.1 Controller 层 + +职责: + +- 接收 HTTP 请求 +- 做基础参数校验 +- 调用应用服务 +- 返回统一响应体 + +约束: + +- 不直接承载复杂业务规则 +- 不直接拼接复杂数据库逻辑 + +### 7.2 Application Service 层 + +职责: + +- 编排业务流程 +- 协调多个领域对象、外部系统、事务边界 +- 对接 Mapper/Repository + +建议: + +- 当前 `service/impl` 可逐步向“应用服务 + 领域服务”演进 + +### 7.3 Domain Rule 层 + +职责: + +- 放置核心业务规则 +- 例如:重复校验、状态切换、编辑权限判定、推送条件判断 + +建议: + +- 先从复杂模块开始,例如商机推送、日报回写、拓展重复校验 + +### 7.4 Repository / Mapper 层 + +职责: + +- 承接数据库查询和持久化 +- 隔离 SQL 与业务逻辑 + +建议: + +- 查询 SQL 与写入 SQL 适当拆分 +- 对复杂列表查询建立专门 Mapper 方法,不在服务层拼装 + +## 8. 前端技术规划 + +### 8.1 路由与页面拆分 + +建议维持当前路由结构,但将复杂页面拆成子模块: + +- `Expansion.tsx` 拆为列表区、表单区、详情区、导出逻辑 +- `Opportunities.tsx` 拆为列表、详情、推送弹窗、编辑弹窗 +- `Work.tsx` 拆为打卡面板、日报面板、历史面板、对象选择器 + +### 8.2 API 层治理 + +建议继续以 `lib/auth.ts` 作为 API 网关入口,但逐步按域拆文件: + +- `api/dashboard.ts` +- `api/expansion.ts` +- `api/opportunity.ts` +- `api/work.ts` +- `api/profile.ts` + +收益: + +- 便于维护 +- 更清晰的类型边界 +- 更方便单元测试和 mock + +### 8.3 组件规划 + +建议将高复用部分抽出: + +- 弹窗容器 +- 列表卡片 +- 表单字段组件 +- 状态标签组件 +- 导出按钮组件 +- 历史记录详情组件 + +### 8.4 前端质量治理 + +建议补齐以下内容: + +- ESLint 与 Prettier 统一规范 +- 类型检查纳入 CI +- 关键页面冒烟测试 +- API mock 机制沉淀为正式脚本 + +## 9. 后端技术规划 + +### 9.1 单体后端演进路线 + +当前推荐继续使用单体服务,建议按模块增强而不是拆服务。 + +演进方向: + +- 强化模块边界 +- 减少跨模块直接访问 +- 将外部系统集成收敛到独立适配层 +- 增加服务层测试与接口测试 + +### 9.2 DTO 与接口契约 + +当前 DTO 已按业务域拆包,建议继续坚持: + +- 输入 DTO 与输出 DTO 分离 +- 复杂聚合返回专门建模 +- 避免前端直接依赖数据库字段命名 + +### 9.3 事务与一致性 + +重点关注场景: + +- 商机保存与外部系统推送 +- 日报提交与商机回写 +- 拓展编辑与关联信息刷新 +- 打卡上传文件与数据库记录保存 + +建议: + +- 本地事务和外部调用解耦 +- 外部系统调用失败时保留本地状态与失败日志 +- 对推送行为增加状态记录表或审计表 + +## 10. 数据库规划 + +### 10.1 设计原则 + +- 主表负责业务主状态 +- 过程表负责轨迹沉淀 +- 审计字段默认标准化 +- 删除操作优先逻辑删除或状态化 +- 高频查询字段提前建索引 + +### 10.2 索引规划建议 + +建议重点优化以下查询: + +- 首页待办查询 +- 首页动态查询 +- 商机列表按负责人、阶段、归档状态查询 +- 拓展列表按负责人、姓名、渠道名查询 +- 打卡/日报历史按用户、日期倒序查询 + +### 10.3 归档与清理规划 + +建议对以下数据建立归档策略: + +- 已归档商机 +- 早期历史动态日志 +- 历史打卡照片 +- 大体量日报附件或冗余快照 + +## 11. 接口治理规划 + +### 11.1 接口规范 + +建议保持统一 API 风格: + +- URL 使用业务域前缀 +- GET 用于查询 +- POST 用于新增或业务动作 +- PUT 用于更新 +- 统一返回结构 `code / msg / data` + +### 11.2 错误处理 + +建议错误分层: + +- 参数错误 +- 业务规则错误 +- 鉴权错误 +- 外部系统错误 +- 系统内部错误 + +并在日志中保留: + +- traceId +- userId +- requestPath +- requestParams +- errorCode +- rootCause + +### 11.3 版本治理 + +当前系统体量不大,可暂不引入 `/v1` 路径版本。 + +但建议提前约定: + +- 外部开放接口预留版本化策略 +- 集成接口变更必须同步更新文档 +- 高风险字段变更必须保持向后兼容 + +## 12. 安全规划 + +### 12.1 鉴权 + +当前系统基于 JWT 鉴权,建议继续强化: + +- Access Token 有效期控制 +- Refresh Token 生命周期管理 +- 登录失败次数控制 +- 敏感操作二次确认 + +### 12.2 数据权限 + +建议明确至少三类权限边界: + +- 仅本人可编辑 +- 本部门可查看 +- 管理员可全局查看 + +### 12.3 配置安全 + +建议将敏感配置从默认配置文件中进一步外置: + +- 数据库密码 +- Redis 密码 +- JWT Secret +- 内部系统 Secret +- 外部系统 API Key + +### 12.4 上传安全 + +打卡照片上传需重点关注: + +- 文件类型限制 +- 文件大小限制 +- 文件名安全 +- 存储目录隔离 +- 访问权限控制 + +## 13. 日志、监控与可观测性规划 + +建议建立以下可观测能力: + +### 13.1 日志 + +- 应用日志 +- 接口访问日志 +- 外部系统调用日志 +- 业务动作日志 +- 失败日志 + +### 13.2 监控指标 + +- 接口响应时间 +- 错误率 +- 慢 SQL +- PostgreSQL 连接数 +- Redis 可用性 +- 文件上传失败率 +- 外部系统推送成功率 + +### 13.3 告警建议 + +- 登录异常激增 +- 商机推送失败率异常 +- 文件上传失败率异常 +- 数据库连接异常 +- 首页聚合接口超时 + +## 14. 测试规划 + +建议建立三层测试体系: + +### 14.1 单元测试 + +覆盖重点: + +- 业务规则判断 +- 状态切换 +- 参数转换 +- 重复校验 + +### 14.2 接口测试 + +覆盖重点: + +- 登录 +- 首页接口 +- 拓展新增/编辑 +- 商机新增/编辑/推送 +- 打卡与日报提交流程 + +### 14.3 前端冒烟测试 + +覆盖重点: + +- 登录页 +- 首页 +- 拓展页 +- 商机页 +- 打卡页 +- 日报页 +- 个人中心页 + +## 15. 部署与环境规划 + +### 15.1 环境分层建议 + +建议至少分为: + +- 本地开发环境 +- 测试环境 +- 预发布环境 +- 生产环境 + +### 15.2 部署建议 + +前端建议: + +- 使用 Nginx 托管静态资源 +- 区分环境配置 +- 启用 gzip 与缓存策略 + +后端建议: + +- 使用独立配置文件或环境变量 +- 使用进程守护或容器部署 +- 按环境区分日志目录、上传目录和外部地址 + +数据库建议: + +- 定期备份 +- 版本化执行 SQL +- 升级前回滚预案 + +## 16. 迭代路线建议 + +### 第一阶段:稳态交付 + +目标: + +- 完成现有主链路稳定交付 +- 收敛环境配置 +- 完成基础测试清单 + +建议事项: + +- 修正高复杂度页面拆分 +- 完善日志与异常信息 +- 固化部署手册与回归手册 + +### 第二阶段:治理增强 + +目标: + +- 增强可维护性与可观测性 + +建议事项: + +- 引入 CI 检查 +- 完成 API 分层拆分 +- 增加推送日志和审计机制 +- 增强权限模型 + +### 第三阶段:业务扩展 + +目标: + +- 支撑更大规模的 CRM 业务演进 + +建议事项: + +- 引入客户主档视图 +- 引入审批/流程节点 +- 引入经营分析与管理报表 +- 引入更标准的数据字典中心 + +## 17. 当前主要技术风险 + +建议重点关注以下风险: + +1. 前端复杂页面单文件过大,维护成本上升。 +2. 外部系统集成失败场景的状态闭环仍需增强。 +3. 当前配置文件中存在敏感参数,生产治理需加强。 +4. 数据权限与角色权限模型还需要形成正式方案。 +5. 自动化测试和持续集成能力仍有较大提升空间。 + +## 18. 技术文档交付建议 + +建议后续完整技术文档体系至少包括: + +- 技术系统规划与技术文档 +- 部署手册 +- 数据库设计说明 +- 接口文档 +- 测试方案与回归清单 +- 运维巡检与故障处理手册 + +## 19. 相关文件 + +- `docs/system-construction.md` +- `docs/business-schema-design.md` +- `docs/deployment-guide.md` +- `docs/opportunity-integration-api.md` +- `backend/README.md` +- `frontend/README.md` + diff --git a/docs/crm操作手册.pdf b/docs/crm操作手册.pdf new file mode 100644 index 00000000..c6d2b38e Binary files /dev/null and b/docs/crm操作手册.pdf differ diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md deleted file mode 100644 index ad62fe66..00000000 --- a/docs/deployment-guide.md +++ /dev/null @@ -1,103 +0,0 @@ -# 全新环境部署说明 - -## 目标 - -本仓库已经把 CRM 自有业务表整理为单一入口脚本: - -- `sql/init_full_pg17.sql` - -后续部署 CRM 表时,优先只执行这一份脚本即可。历史迁移脚本已归档到: - -- `sql/archive/` - -## 执行顺序 - -### 1. 准备基础环境 - -需要先准备: - -- PostgreSQL 17 -- Redis -- Java 17+ -- Maven 3.9+ - -### 2. 先初始化基础框架(UnisBase)表和基础数据 - -这一步不在本仓库维护,需要先由基础框架提供方执行。 - -当前 CRM 代码运行时会依赖下列基础表或基础数据: - -- `sys_user` -- `sys_org` -- `sys_dict_item` -- `sys_tenant_user` -- `sys_role` -- `sys_user_role` -- `sys_param` -- `sys_log` -- `device` - -如果这些表或基础数据未准备完成,CRM 即使业务表建好了,也会在登录、字典加载、组织信息、角色信息等链路上报错。 - -### 3. 执行 CRM 单文件初始化脚本 - -```bash -psql -h 127.0.0.1 -U postgres -d nex_auth -f sql/init_full_pg17.sql -``` - -说明: - -- 这份脚本既适合空库初始化,也兼容大部分旧环境补齐结构 -- 它已经吸收了仓库里原来的 DDL 迁移脚本 -- `sql/archive/` 中的旧脚本仅保留作历史追溯,正常部署不再单独执行 - -### 4. 初始化完成后建议检查的 CRM 表 - -建议至少确认以下表已存在: - -- `crm_customer` -- `crm_opportunity` -- `crm_opportunity_followup` -- `crm_sales_expansion` -- `crm_channel_expansion` -- `crm_channel_expansion_contact` -- `crm_expansion_followup` -- `work_checkin` -- `work_daily_report` -- `work_daily_report_comment` -- `work_todo` -- `sys_activity_log` - -### 5. 启动后端服务 - -```bash -cd backend -mvn spring-boot:run -``` - -默认端口: - -- `8080` - -### 6. 启动前端并验证 - -建议至少验证以下链路: - -- 登录 -- 首页统计、待办、动态 -- 拓展列表与详情 -- 商机列表与详情 -- 工作台打卡、日报、历史记录 - -## 升级已有环境时的建议 - -如果不是全新环境,而是已有库升级: - -1. 先备份数据库 -2. 直接执行 `sql/init_full_pg17.sql` -3. 验证登录、字典、组织、日报、商机、拓展几条主链路 - -## 当前目录约定 - -- `sql/init_full_pg17.sql`:唯一部署入口脚本 -- `sql/archive/`:历史迁移与修数脚本归档目录 diff --git a/docs/scripts/capture-manual-screenshots.mjs b/docs/scripts/capture-manual-screenshots.mjs new file mode 100644 index 00000000..b07096f6 --- /dev/null +++ b/docs/scripts/capture-manual-screenshots.mjs @@ -0,0 +1,758 @@ +import fs from "fs/promises"; +import path from "path"; +import { chromium } from "playwright"; + +const baseUrl = process.env.CRM_DOC_BASE_URL || "http://127.0.0.1:3001"; +const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", ".."); +const outputDir = path.join(rootDir, "docs", "assets", "manual"); + +const currentUser = { + userId: 1, + tenantId: 1001, + username: "zhangsan", + displayName: "张三", + email: "zhangsan@uniscrm.example", + phone: "13800138000", + roleCodes: ["crm_sales"], + roles: [{ roleCode: "crm_sales", roleName: "销售经理" }], +}; + +const dashboardHome = { + userId: 1, + realName: "张三", + jobTitle: "销售经理", + deptName: "华东销售一部", + onboardingDays: 386, + stats: [ + { name: "本月新增商机", value: 12, metricKey: "monthlyOpportunities" }, + { name: "已推送OMS项目", value: 5, metricKey: "pushedOmsProjects" }, + { name: "本月新增渠道", value: 8, metricKey: "monthlyChannels" }, + { name: "本月打卡次数", value: 19, metricKey: "monthlyCheckins" }, + ], + todos: [ + { id: "todo-1", title: "跟进华东教育云桌面项目", status: "pending" }, + { id: "todo-2", title: "补充苏州金桥渠道资料", status: "pending" }, + { id: "todo-3", title: "完成3月销售日报点评回复", status: "done" }, + { id: "todo-4", title: "更新杭州政务项目推进记录", status: "done" }, + ], + activities: [ + { id: 1, title: "新增商机", content: "华东教育云桌面项目已完成立项登记。", timeText: "今天 09:18" }, + { id: 2, title: "渠道跟进", content: "苏州金桥渠道补充了联系人与办公地址。", timeText: "今天 10:42" }, + { id: 3, title: "日报提交", content: "李四已提交今日销售日报,等待点评。", timeText: "今天 18:11" }, + { id: 4, title: "OMS推送", content: "杭州政务云项目已推送至 OMS。", timeText: "昨天 16:25" }, + { id: 5, title: "打卡完成", content: "张三完成外勤拜访打卡,地点为上海市徐汇区。", timeText: "昨天 14:06" }, + { id: 6, title: "拓展新增", content: "新增销售拓展对象:王磊。", timeText: "昨天 11:30" }, + ], +}; + +const profileOverview = { + userId: 1, + monthlyOpportunityCount: 12, + monthlyExpansionCount: 8, + averageScore: 96, + onboardingDays: 386, + realName: "张三", + jobTitle: "销售经理", + deptName: "华东销售一部", + accountStatus: "正常", +}; + +const expansionOverview = { + salesItems: [ + { + id: 11, + ownerUserId: 1, + owner: "张三", + type: "sales", + employeeNo: "S2024001", + name: "王磊", + officeName: "上海代表处", + phone: "13900001111", + dept: "教育行业部", + industry: "教育", + title: "销售总监", + intentLevel: "high", + intent: "高", + hasExp: true, + active: true, + relatedProjects: [ + { opportunityId: 101, opportunityCode: "OPP-2026-001", opportunityName: "华东教育云桌面项目", amount: 860000 }, + ], + followUps: [ + { + id: 1, + date: "2026-04-02", + type: "拜访", + visitStartTime: "14:00", + evaluationContent: "已确认预算窗口与技术负责人。", + nextPlan: "下周安排方案交流。", + }, + ], + }, + { + id: 12, + ownerUserId: 1, + owner: "张三", + type: "sales", + employeeNo: "S2024002", + name: "李敏", + officeName: "杭州办事处", + phone: "13900002222", + dept: "政企行业部", + industry: "政府", + title: "客户经理", + intentLevel: "medium", + intent: "中", + hasExp: false, + active: true, + relatedProjects: [], + followUps: [], + }, + ], + channelItems: [ + { + id: 21, + ownerUserId: 1, + owner: "张三", + type: "channel", + channelCode: "CH-2026-001", + name: "苏州金桥科技有限公司", + province: "江苏省", + city: "苏州市", + officeAddress: "苏州市工业园区星湖街 188 号", + certificationLevel: "银牌", + channelIndustry: "教育、政府", + channelAttribute: "区域渠道", + internalAttribute: "重点合作伙伴", + intentLevel: "high", + intent: "高", + establishedDate: "2026-03-12", + revenue: "3000 万", + size: 120, + hasDesktopExp: true, + contacts: [ + { id: 1, name: "刘晨", mobile: "13700001111", title: "总经理" }, + { id: 2, name: "顾宇", mobile: "13700002222", title: "技术总监" }, + ], + relatedProjects: [ + { opportunityId: 102, opportunityCode: "OPP-2026-002", opportunityName: "杭州政务云项目", amount: 1250000 }, + ], + followUps: [ + { + id: 11, + date: "2026-04-01", + type: "电话沟通", + visitStartTime: "10:30", + evaluationContent: "已同步合作模式与首批目标客户。", + nextPlan: "4月中旬安排联合拜访。", + }, + ], + }, + { + id: 22, + ownerUserId: 1, + owner: "张三", + type: "channel", + channelCode: "CH-2026-002", + name: "宁波数智集成服务商", + province: "浙江省", + city: "宁波市", + officeAddress: "宁波市高新区研发园 B 座", + certificationLevel: "金牌", + channelIndustry: "制造", + channelAttribute: "行业渠道", + internalAttribute: "储备伙伴", + intentLevel: "medium", + intent: "中", + establishedDate: "2026-02-23", + revenue: "5000 万", + size: 80, + hasDesktopExp: false, + contacts: [{ id: 3, name: "郑航", mobile: "13700003333", title: "商务经理" }], + relatedProjects: [], + followUps: [], + }, + ], +}; + +const expansionMeta = { + officeOptions: [ + { label: "上海代表处", value: "上海代表处" }, + { label: "杭州办事处", value: "杭州办事处" }, + { label: "苏州办事处", value: "苏州办事处" }, + ], + industryOptions: [ + { label: "教育", value: "教育" }, + { label: "政府", value: "政府" }, + { label: "制造", value: "制造" }, + { label: "医疗", value: "医疗" }, + ], + provinceOptions: [ + { label: "上海市", value: "上海市" }, + { label: "江苏省", value: "江苏省" }, + { label: "浙江省", value: "浙江省" }, + ], + certificationLevelOptions: [ + { label: "金牌", value: "金牌" }, + { label: "银牌", value: "银牌" }, + { label: "注册", value: "注册" }, + ], + channelAttributeOptions: [ + { label: "区域渠道", value: "区域渠道" }, + { label: "行业渠道", value: "行业渠道" }, + { label: "其他", value: "其他" }, + ], + internalAttributeOptions: [ + { label: "重点合作伙伴", value: "重点合作伙伴" }, + { label: "储备伙伴", value: "储备伙伴" }, + ], + nextChannelCode: "CH-2026-003", +}; + +const opportunityOverview = { + items: [ + { + id: 101, + ownerUserId: 1, + code: "OPP-2026-001", + name: "华东教育云桌面项目", + client: "华东某职业学院", + owner: "张三", + projectLocation: "上海市", + operatorName: "新华三+渠道", + amount: 860000, + date: "2026-05-20", + confidence: "A", + stageCode: "方案交流", + stage: "方案交流", + type: "新建", + archived: false, + pushedToOms: true, + product: "VDI云桌面", + source: "主动开发", + salesExpansionId: 11, + salesExpansionName: "王磊", + channelExpansionId: 21, + channelExpansionName: "苏州金桥科技有限公司", + preSalesId: 2001, + preSalesName: "赵工", + competitorName: "华为、深信服", + latestProgress: "客户已确认方案范围,准备进入报价阶段。", + nextPlan: "下周提交正式报价并安排演示环境。", + notes: "项目纳入二季度重点跟踪清单。", + followUps: [ + { + id: 1, + date: "2026-04-02", + type: "拜访", + latestProgress: "完成需求澄清", + communicationContent: "客户希望 5 月底前完成采购流程。", + nextAction: "输出报价清单", + user: "张三", + }, + ], + }, + { + id: 102, + ownerUserId: 1, + code: "OPP-2026-002", + name: "杭州政务云项目", + client: "杭州某政务中心", + owner: "张三", + projectLocation: "浙江省", + operatorName: "渠道", + amount: 1250000, + date: "2026-06-15", + confidence: "B", + stageCode: "商机储备", + stage: "商机储备", + type: "扩容", + archived: false, + pushedToOms: false, + product: "VDI云桌面", + source: "渠道引入", + channelExpansionId: 21, + channelExpansionName: "苏州金桥科技有限公司", + competitorName: "锐捷", + latestProgress: "等待客户确认立项时间。", + nextPlan: "继续维护客户关键人。", + notes: "渠道主导推进。", + followUps: [], + }, + { + id: 103, + ownerUserId: 1, + code: "OPP-2026-003", + name: "宁波制造云桌面替换项目", + client: "宁波某制造集团", + owner: "张三", + projectLocation: "浙江省", + operatorName: "新华三", + amount: 530000, + date: "2026-03-18", + confidence: "C", + stageCode: "已归档", + stage: "已归档", + type: "替换", + archived: true, + pushedToOms: false, + product: "VDI云桌面", + source: "主动开发", + salesExpansionId: 12, + salesExpansionName: "李敏", + competitorName: "无", + latestProgress: "客户预算取消,项目归档。", + nextPlan: "", + notes: "保留历史记录。", + followUps: [], + }, + ], +}; + +const opportunityMeta = { + stageOptions: [ + { label: "商机储备", value: "商机储备" }, + { label: "方案交流", value: "方案交流" }, + { label: "报价申请", value: "报价申请" }, + { label: "合同审批", value: "合同审批" }, + ], + operatorOptions: [ + { label: "新华三", value: "新华三" }, + { label: "渠道", value: "渠道" }, + { label: "新华三+渠道", value: "新华三+渠道" }, + ], + projectLocationOptions: [ + { label: "上海市", value: "上海市" }, + { label: "江苏省", value: "江苏省" }, + { label: "浙江省", value: "浙江省" }, + ], + opportunityTypeOptions: [ + { label: "新建", value: "新建" }, + { label: "扩容", value: "扩容" }, + { label: "替换", value: "替换" }, + ], +}; + +const workOverview = { + todayCheckIn: { + id: 501, + bizType: "opportunity", + bizId: 101, + bizName: "华东教育云桌面项目", + userName: "张三", + deptName: "华东销售一部", + }, + todayReport: { + id: 601, + status: "submitted", + workContent: "今日完成客户拜访、商机澄清与渠道协同推进。", + lineItems: [ + { + workDate: "2026-04-03", + bizType: "opportunity", + bizId: 101, + bizName: "华东教育云桌面项目", + content: "客户已明确首批终端规模,进入报价准备阶段。", + latestProgress: "方案范围已确认", + nextPlan: "下周提交报价与演示计划", + }, + { + workDate: "2026-04-03", + bizType: "channel", + bizId: 21, + bizName: "苏州金桥科技有限公司", + content: "同步联合拓展节奏,确认4月联合拜访名单。", + evaluationContent: "渠道配合度较高", + nextPlan: "输出联合拜访安排", + }, + ], + planItems: [ + { content: "提交华东教育项目报价草案" }, + { content: "完成杭州政务项目客户关系梳理" }, + ], + tomorrowPlan: "围绕重点项目推进报价与客户触达。", + sourceType: "manual", + score: 98, + comment: "内容完整,继续保持。", + }, +}; + +const checkinHistoryItems = [ + { + id: 1, + type: "外勤打卡", + date: "2026-04-03", + time: "14:06", + status: "已提交", + content: "打卡人:张三\n关联对象:华东教育云桌面项目\n地址:上海市徐汇区桂平路 680 号\n备注:已完成现场拜访。", + photoUrls: [], + }, + { + id: 2, + type: "外勤打卡", + date: "2026-04-02", + time: "16:20", + status: "已提交", + content: "打卡人:张三\n关联对象:苏州金桥科技有限公司\n地址:苏州市工业园区星湖街 188 号\n备注:渠道联合沟通。", + photoUrls: [], + }, +]; + +const reportHistoryItems = [ + { + id: 3, + type: "销售日报", + date: "2026-04-03", + time: "18:11", + status: "已点评", + score: 98, + comment: "内容完整,继续保持。", + content: "提交人:张三\n今日工作:推进华东教育项目与渠道协同。\n明日计划:准备报价并继续客户触达。", + }, + { + id: 4, + type: "销售日报", + date: "2026-04-02", + time: "18:05", + status: "已提交", + score: 95, + comment: "", + content: "提交人:张三\n今日工作:商机梳理与外勤拜访。\n明日计划:整理客户需求与推进方案。", + }, +]; + +function apiEnvelope(data, msg = "success") { + return { + code: "0", + msg, + data, + }; +} + +function createToken() { + const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url"); + const payload = Buffer.from(JSON.stringify({ userId: currentUser.userId, tenantId: currentUser.tenantId })).toString("base64url"); + return `${header}.${payload}.mock-signature`; +} + +function createCaptchaSvgBase64() { + const svg = ` + + + 2468 + + + + `.trim(); + return Buffer.from(svg).toString("base64"); +} + +async function installApiMocks(page) { + await page.route("**/*", async (route) => { + const requestUrl = route.request().url(); + const url = new URL(requestUrl); + + if (!url.pathname.startsWith("/api/")) { + if ( + requestUrl.startsWith("https://mapapi.qq.com/") || + requestUrl.startsWith("https://map.qq.com/") || + requestUrl.startsWith("https://res.wx.qq.com/") || + requestUrl.startsWith("https://open.work.weixin.qq.com/") + ) { + await route.abort(); + return; + } + await route.continue(); + return; + } + + const pathname = url.pathname; + const method = route.request().method().toUpperCase(); + + if (pathname === "/api/sys/auth/captcha") { + await route.fulfill({ json: apiEnvelope({ captchaId: "mock-captcha-id", imageBase64: createCaptchaSvgBase64() }) }); + return; + } + + if (pathname === "/api/sys/auth/login" && method === "POST") { + await route.fulfill({ + json: apiEnvelope({ + accessToken: createToken(), + refreshToken: "mock-refresh-token", + accessExpiresInMinutes: 120, + refreshExpiresInDays: 7, + }), + }); + return; + } + + if (pathname === "/api/sys/api/params/value") { + await route.fulfill({ json: apiEnvelope("true") }); + return; + } + + if (pathname === "/api/sys/api/open/platform/config") { + await route.fulfill({ + json: apiEnvelope({ + projectName: "紫光汇智CRM系统", + systemDescription: "聚焦客户拓展、商机推进与销售协同,让团队每天的工作节奏更清晰。", + }), + }); + return; + } + + if (pathname === "/api/sys/api/users/me") { + await route.fulfill({ json: apiEnvelope(currentUser) }); + return; + } + + if (pathname === "/api/dashboard/home") { + await route.fulfill({ json: apiEnvelope(dashboardHome) }); + return; + } + + if (pathname.startsWith("/api/dashboard/todos/") && pathname.endsWith("/complete")) { + await route.fulfill({ json: apiEnvelope(null) }); + return; + } + + if (pathname === "/api/profile/overview") { + await route.fulfill({ json: apiEnvelope(profileOverview) }); + return; + } + + if (pathname === "/api/sys/api/users/profile" && method === "PUT") { + await route.fulfill({ json: apiEnvelope(true) }); + return; + } + + if (pathname === "/api/sys/api/users/password" && method === "PUT") { + await route.fulfill({ json: apiEnvelope(true) }); + return; + } + + if (pathname === "/api/work/overview") { + await route.fulfill({ json: apiEnvelope(workOverview) }); + return; + } + + if (pathname === "/api/work/history") { + const type = url.searchParams.get("type") || "checkin"; + const pageNo = Number(url.searchParams.get("page") || "1"); + const allItems = type === "report" ? reportHistoryItems : checkinHistoryItems; + const items = pageNo === 1 ? allItems : []; + await route.fulfill({ + json: apiEnvelope({ + items, + hasMore: false, + page: pageNo, + size: 8, + }), + }); + return; + } + + if (pathname === "/api/work/reverse-geocode") { + await route.fulfill({ json: apiEnvelope("上海市徐汇区桂平路 680 号 创新大厦") }); + return; + } + + if (pathname === "/api/work/checkins" && method === "POST") { + await route.fulfill({ json: apiEnvelope(7001) }); + return; + } + + if (pathname === "/api/work/checkin-photos" && method === "POST") { + await route.fulfill({ json: apiEnvelope("/mock/checkin-photo.png") }); + return; + } + + if (pathname === "/api/work/daily-reports" && method === "POST") { + await route.fulfill({ json: apiEnvelope(7002) }); + return; + } + + if (pathname === "/api/expansion/overview") { + await route.fulfill({ json: apiEnvelope(expansionOverview) }); + return; + } + + if (pathname === "/api/expansion/meta") { + await route.fulfill({ json: apiEnvelope(expansionMeta) }); + return; + } + + if (pathname === "/api/expansion/areas/cities") { + const provinceName = url.searchParams.get("provinceName"); + const cities = provinceName === "江苏省" + ? [{ label: "苏州市", value: "苏州市" }, { label: "南京市", value: "南京市" }] + : provinceName === "浙江省" + ? [{ label: "杭州市", value: "杭州市" }, { label: "宁波市", value: "宁波市" }] + : [{ label: "上海市", value: "上海市" }]; + await route.fulfill({ json: apiEnvelope(cities) }); + return; + } + + if ( + pathname === "/api/expansion/sales/duplicate-check" || + pathname === "/api/expansion/channel/duplicate-check" + ) { + await route.fulfill({ json: apiEnvelope({ duplicated: false, message: "" }) }); + return; + } + + if (pathname.startsWith("/api/expansion/") && ["POST", "PUT"].includes(method)) { + await route.fulfill({ json: apiEnvelope(1) }); + return; + } + + if (pathname === "/api/opportunities/overview") { + await route.fulfill({ json: apiEnvelope(opportunityOverview) }); + return; + } + + if (pathname === "/api/opportunities/meta") { + await route.fulfill({ json: apiEnvelope(opportunityMeta) }); + return; + } + + if (pathname === "/api/opportunities/oms/pre-sales") { + await route.fulfill({ + json: apiEnvelope([ + { userId: 2001, loginName: "zhaogong", userName: "赵工" }, + { userId: 2002, loginName: "wanggong", userName: "王工" }, + ]), + }); + return; + } + + if (pathname === "/api/opportunities" && method === "POST") { + await route.fulfill({ json: apiEnvelope(9001) }); + return; + } + + if (pathname.startsWith("/api/opportunities/") && pathname.endsWith("/push-oms")) { + await route.fulfill({ json: apiEnvelope(1) }); + return; + } + + if (pathname.startsWith("/api/opportunities/") && ["PUT", "POST"].includes(method)) { + await route.fulfill({ json: apiEnvelope(1) }); + return; + } + + console.warn(`Unhandled API mock: ${method} ${pathname}`); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(apiEnvelope(null)), + }); + }); +} + +async function seedAuth(page) { + const token = createToken(); + await page.addInitScript( + ({ user, accessToken }) => { + localStorage.setItem("accessToken", accessToken); + localStorage.setItem("refreshToken", "mock-refresh-token"); + localStorage.setItem("username", user.username); + sessionStorage.setItem("userProfile", JSON.stringify(user)); + }, + { user: currentUser, accessToken: token }, + ); +} + +async function waitForStablePage(page) { + await page.waitForLoadState("networkidle"); + await page.waitForTimeout(800); +} + +async function capture(page, pathname, fileName, readyText, options = {}) { + console.log(`Capturing ${fileName} from ${pathname}`); + await page.goto(`${baseUrl}${pathname}`, { waitUntil: "domcontentloaded" }); + if (readyText) { + await page.getByText(readyText, { exact: false }).first().waitFor({ state: "visible", timeout: 15000 }); + } + await waitForStablePage(page); + await page.screenshot({ + path: path.join(outputDir, fileName), + fullPage: true, + ...options, + }); +} + +async function clickButtonByText(page, text) { + const locator = page.getByRole("button", { name: new RegExp(text) }).first(); + await locator.waitFor({ state: "visible", timeout: 15000 }); + await locator.click(); +} + +async function main() { + await fs.mkdir(outputDir, { recursive: true }); + + const browser = await chromium.launch({ + channel: "chrome", + headless: true, + }); + + const loginContext = await browser.newContext({ + viewport: { width: 1440, height: 1200 }, + deviceScaleFactor: 1, + locale: "zh-CN", + }); + const loginPage = await loginContext.newPage(); + await installApiMocks(loginPage); + await capture(loginPage, "/login", "01-login.png", "紫光汇智CRM系统"); + + const appContext = await browser.newContext({ + viewport: { width: 1440, height: 1600 }, + deviceScaleFactor: 1, + locale: "zh-CN", + colorScheme: "light", + geolocation: { latitude: 31.178744, longitude: 121.410428 }, + permissions: ["geolocation"], + }); + const appPage = await appContext.newPage(); + await installApiMocks(appPage); + await seedAuth(appPage); + + await capture(appPage, "/", "02-dashboard.png", "工作台"); + await capture(appPage, "/expansion", "03-expansion-sales.png", "销售人员拓展"); + + await clickButtonByText(appPage, "新增"); + await appPage.getByRole("heading", { name: "新增销售人员拓展" }).waitFor({ state: "visible", timeout: 15000 }); + await waitForStablePage(appPage); + console.log("Capturing 04-expansion-create.png from modal"); + await appPage.screenshot({ path: path.join(outputDir, "04-expansion-create.png"), fullPage: true }); + await clickButtonByText(appPage, "取消"); + await waitForStablePage(appPage); + + await appPage.getByRole("button", { name: "渠道拓展" }).click(); + await appPage.getByText("苏州金桥科技有限公司").waitFor({ state: "visible", timeout: 15000 }); + await waitForStablePage(appPage); + console.log("Capturing 05-expansion-channel.png from channel tab"); + await appPage.screenshot({ path: path.join(outputDir, "05-expansion-channel.png"), fullPage: true }); + + await capture(appPage, "/opportunities", "06-opportunities.png", "商机"); + await clickButtonByText(appPage, "新增商机"); + await appPage.getByRole("heading", { name: "新增商机" }).waitFor({ state: "visible", timeout: 15000 }); + await waitForStablePage(appPage); + console.log("Capturing 07-opportunity-create.png from modal"); + await appPage.screenshot({ path: path.join(outputDir, "07-opportunity-create.png"), fullPage: true }); + await clickButtonByText(appPage, "取消"); + await waitForStablePage(appPage); + + await capture(appPage, "/work/checkin", "08-work-checkin.png", "外勤打卡"); + await capture(appPage, "/work/report", "09-work-report.png", "销售日报"); + await capture(appPage, "/profile", "10-profile.png", "账号安全"); + + await loginContext.close(); + await appContext.close(); + await browser.close(); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/docs/scripts/render-formal-pdfs.mjs b/docs/scripts/render-formal-pdfs.mjs new file mode 100644 index 00000000..e7ad9e26 --- /dev/null +++ b/docs/scripts/render-formal-pdfs.mjs @@ -0,0 +1,691 @@ +import fs from "fs/promises"; +import os from "os"; +import path from "path"; +import { execFile } from "child_process"; +import { promisify } from "util"; + +const execFileAsync = promisify(execFile); + +const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", ".."); +const docsDir = path.join(rootDir, "docs"); +const chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; +const currentDate = "2026-04-03"; + +const documents = [ + { + source: path.join(docsDir, "system-operation-manual.md"), + output: path.join(docsDir, "system-operation-manual.pdf"), + docCode: "UNIS-CRM-UM-2026", + subtitle: "正式交付版", + audience: "业务用户 / 管理人员", + }, + { + source: path.join(docsDir, "system-construction.md"), + output: path.join(docsDir, "system-construction.pdf"), + docCode: "UNIS-CRM-BUILD-2026", + subtitle: "正式交付版", + audience: "项目组 / 运维 / 交付人员", + }, +]; + +function escapeHtml(value) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function inlineMarkdownToHtml(text, baseDir) { + let html = escapeHtml(text); + + html = html.replace(/`([^`]+)`/g, (_, code) => `${escapeHtml(code)}`); + html = html.replace(/\*\*([^*]+)\*\*/g, "$1"); + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => { + const resolved = resolveHref(href, baseDir); + return `${escapeHtml(label)}`; + }); + + return html; +} + +function resolveHref(href, baseDir) { + if (/^(https?:|file:|data:|#)/i.test(href)) { + return href; + } + const absolutePath = path.resolve(baseDir, href); + return `file://${absolutePath}`; +} + +function slugify(text, fallbackIndex) { + const normalized = text + .toLowerCase() + .replace(/[`~!@#$%^&*()+=[\]{};:'"\\|,.<>/?,。;:()【】《》?、\s]+/g, "-") + .replace(/^-+|-+$/g, ""); + return normalized || `section-${fallbackIndex}`; +} + +function extractTitle(markdown) { + const match = markdown.match(/^#\s+(.+)$/m); + return match ? match[1].trim() : "文档"; +} + +function parseBlocks(markdown, baseDir) { + const lines = markdown.replace(/\r\n/g, "\n").split("\n"); + const blocks = []; + let index = 0; + + while (index < lines.length) { + const line = lines[index]; + const trimmed = line.trim(); + + if (!trimmed) { + index += 1; + continue; + } + + const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/); + if (headingMatch) { + blocks.push({ + type: "heading", + level: headingMatch[1].length, + text: headingMatch[2].trim(), + }); + index += 1; + continue; + } + + const codeMatch = trimmed.match(/^```(\w+)?$/); + if (codeMatch) { + const language = codeMatch[1] || ""; + index += 1; + const content = []; + while (index < lines.length && !lines[index].trim().startsWith("```")) { + content.push(lines[index]); + index += 1; + } + if (index < lines.length) { + index += 1; + } + blocks.push({ + type: "code", + language, + content: content.join("\n"), + }); + continue; + } + + const imageMatch = trimmed.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); + if (imageMatch) { + blocks.push({ + type: "image", + alt: imageMatch[1].trim(), + src: resolveHref(imageMatch[2].trim(), baseDir), + }); + index += 1; + continue; + } + + if ( + trimmed.includes("|") && + index + 1 < lines.length && + /^\|?[\s:-]+(\|[\s:-]+)+\|?$/.test(lines[index + 1].trim()) + ) { + const tableLines = [trimmed]; + index += 2; + while (index < lines.length && lines[index].trim().includes("|")) { + tableLines.push(lines[index].trim()); + index += 1; + } + blocks.push({ + type: "table", + rows: tableLines.map((row) => + row + .replace(/^\|/, "") + .replace(/\|$/, "") + .split("|") + .map((cell) => cell.trim()), + ), + }); + continue; + } + + const listMatch = trimmed.match(/^([-*]|\d+\.)\s+(.+)$/); + if (listMatch) { + const ordered = /\d+\./.test(listMatch[1]); + const items = []; + while (index < lines.length) { + const itemMatch = lines[index].trim().match(/^([-*]|\d+\.)\s+(.+)$/); + if (!itemMatch) { + break; + } + items.push(itemMatch[2].trim()); + index += 1; + } + blocks.push({ + type: "list", + ordered, + items, + }); + continue; + } + + const paragraphLines = [trimmed]; + index += 1; + while (index < lines.length) { + const next = lines[index].trim(); + if ( + !next || + /^#{1,6}\s+/.test(next) || + /^```/.test(next) || + /^!\[/.test(next) || + /^([-*]|\d+\.)\s+/.test(next) || + ( + next.includes("|") && + index + 1 < lines.length && + /^\|?[\s:-]+(\|[\s:-]+)+\|?$/.test(lines[index + 1].trim()) + ) + ) { + break; + } + paragraphLines.push(next); + index += 1; + } + blocks.push({ + type: "paragraph", + text: paragraphLines.join(" "), + }); + } + + return blocks; +} + +function buildContentAndToc(blocks, baseDir) { + const toc = []; + const html = []; + let headingIndex = 0; + let skippedTitle = false; + + for (const block of blocks) { + if (block.type === "heading") { + headingIndex += 1; + + if (block.level === 1 && !skippedTitle) { + skippedTitle = true; + continue; + } + + const id = slugify(block.text, headingIndex); + if (block.level <= 3) { + toc.push({ level: block.level, text: block.text, id }); + } + html.push(`${escapeHtml(block.text)}`); + continue; + } + + if (block.type === "paragraph") { + html.push(`

${inlineMarkdownToHtml(block.text, baseDir)}

`); + continue; + } + + if (block.type === "list") { + const tag = block.ordered ? "ol" : "ul"; + const items = block.items + .map((item) => `
  • ${inlineMarkdownToHtml(item, baseDir)}
  • `) + .join(""); + html.push(`<${tag}>${items}`); + continue; + } + + if (block.type === "image") { + html.push( + `
    ${escapeHtml(block.alt ||
    ${escapeHtml(block.alt || "")}
    `, + ); + continue; + } + + if (block.type === "code") { + if (block.language === "mermaid") { + html.push( + `
    架构示意
    ${escapeHtml(block.content)}
    `, + ); + } else { + html.push( + `
    ${escapeHtml(block.content)}
    `, + ); + } + continue; + } + + if (block.type === "table") { + const [headerRow, ...bodyRows] = block.rows; + const thead = `${headerRow.map((cell) => `${inlineMarkdownToHtml(cell, baseDir)}`).join("")}`; + const tbody = `${bodyRows.map((row) => `${row.map((cell) => `${inlineMarkdownToHtml(cell, baseDir)}`).join("")}`).join("")}`; + html.push(`${thead}${tbody}
    `); + } + } + + return { + html: html.join("\n"), + toc, + }; +} + +function renderToc(toc) { + const items = toc + .filter((item) => item.level >= 2 && item.level <= 3) + .map((item) => { + const cls = item.level === 3 ? "toc-item toc-sub" : "toc-item"; + return `
  • ${escapeHtml(item.text)}
  • `; + }) + .join(""); + return `

    目录

    `; +} + +function buildCover(title, options) { + return ` +
    +
    +
    UNIS CRM
    +
    +

    紫光汇智客户关系管理平台

    +

    ${escapeHtml(title)}

    +

    ${escapeHtml(options.subtitle)}

    +
    +
    文档编号${escapeHtml(options.docCode)}
    +
    适用对象${escapeHtml(options.audience)}
    +
    版本状态正式交付版
    +
    编制日期${currentDate}
    +
    +
    +
    + `; +} + +function buildHtml(title, bodyContent, tocHtml, options) { + return ` + + + + + ${escapeHtml(title)} + + + +
    + ${buildCover(title, options)} + ${tocHtml} +
    +
    +
    + UNIS CRM + ${escapeHtml(options.docCode)} +
    +
    + ${escapeHtml(title)} + ${currentDate} +
    +
    +
    说明:本 PDF 为正式交付版排版,内容依据仓库中的 Markdown 文档自动生成。
    + ${bodyContent} +
    +
    + +`; +} + +async function renderDocument(config) { + const markdown = await fs.readFile(config.source, "utf8"); + const title = extractTitle(markdown); + const blocks = parseBlocks(markdown, path.dirname(config.source)); + const { html, toc } = buildContentAndToc(blocks, path.dirname(config.source)); + const fullHtml = buildHtml(title, html, renderToc(toc), config); + const tempHtml = path.join(os.tmpdir(), `${path.basename(config.output, ".pdf")}-${Date.now()}.html`); + + await fs.writeFile(tempHtml, fullHtml, "utf8"); + + try { + await execFileAsync( + chromePath, + [ + "--headless=new", + "--disable-gpu", + "--allow-file-access-from-files", + "--no-pdf-header-footer", + `--print-to-pdf=${config.output}`, + "--print-to-pdf-no-header", + `file://${tempHtml}`, + ], + { + cwd: rootDir, + maxBuffer: 10 * 1024 * 1024, + }, + ); + } finally { + await fs.rm(tempHtml, { force: true }); + } +} + +async function main() { + const requested = process.argv[2]; + const targetDocuments = requested + ? documents.filter((documentConfig) => path.basename(documentConfig.output) === requested || path.basename(documentConfig.source) === requested) + : documents; + + if (requested && targetDocuments.length === 0) { + throw new Error(`未找到需要导出的文档:${requested}`); + } + + for (const documentConfig of targetDocuments) { + console.log(`Rendering ${path.basename(documentConfig.output)} ...`); + await renderDocument(documentConfig); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/docs/ziguang-huizhi-crm-icon-logo.svg b/docs/ziguang-huizhi-crm-icon-logo.svg deleted file mode 100644 index 19dd92c9..00000000 --- a/docs/ziguang-huizhi-crm-icon-logo.svg +++ /dev/null @@ -1,46 +0,0 @@ - - Ziguang Huizhi CRM Icon Logo - Brand-oriented CRM logo icon with an abstract converging symbol, data nodes, and geometric structure in purple tones. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/ziguang-huizhi-crm-icon-minimal.svg b/docs/ziguang-huizhi-crm-icon-minimal.svg deleted file mode 100644 index 11f96844..00000000 --- a/docs/ziguang-huizhi-crm-icon-minimal.svg +++ /dev/null @@ -1,37 +0,0 @@ - - Ziguang Huizhi CRM Icon Minimal - Minimal enterprise CRM icon with geometric links, converging data paths, and a premium purple palette. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/ziguang-huizhi-crm-icon-premium-glow.svg b/docs/ziguang-huizhi-crm-icon-premium-glow.svg deleted file mode 100644 index 64c2c5a0..00000000 --- a/docs/ziguang-huizhi-crm-icon-premium-glow.svg +++ /dev/null @@ -1,86 +0,0 @@ - - Ziguang Huizhi CRM Icon Premium Glow - Premium CRM icon with luminous data rings, glowing nodes, and a glass-like core on a purple enterprise background. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/ziguang-huizhi-crm-icon-system.svg b/docs/ziguang-huizhi-crm-icon-system.svg deleted file mode 100644 index 31797ff7..00000000 --- a/docs/ziguang-huizhi-crm-icon-system.svg +++ /dev/null @@ -1,86 +0,0 @@ - - 紫光汇智 CRM 系统图标增强版 - 以客户节点、管理中枢、数据连接和CRM字样,强化客户关系管理系统的识别度。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/ziguang-huizhi-crm-icon.svg b/docs/ziguang-huizhi-crm-icon.svg deleted file mode 100644 index 1651a9b2..00000000 --- a/docs/ziguang-huizhi-crm-icon.svg +++ /dev/null @@ -1,76 +0,0 @@ - - 紫光汇智 CRM 系统图标 - 以紫色几何网络和数据流为核心,表达汇聚智慧、数据连接与客户关系管理的现代企业级图标。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/ziguang-huizhi-sales-crm-icon.svg b/docs/ziguang-huizhi-sales-crm-icon.svg deleted file mode 100644 index 8d34f75d..00000000 --- a/docs/ziguang-huizhi-sales-crm-icon.svg +++ /dev/null @@ -1,94 +0,0 @@ - - 紫光汇智销售 CRM 系统图标 - 以线索节点、商机漏斗、转化路径和成交中枢,表达销售 CRM 系统的业务特征。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/数据结构.md b/docs/数据结构.md new file mode 100644 index 00000000..6cafec3e --- /dev/null +++ b/docs/数据结构.md @@ -0,0 +1,658 @@ +# 紫光汇智 CRM 数据库表设计文档 + +## 1. 文档说明 + +本文档用于说明紫光汇智 CRM 系统当前数据库表设计,主要依据以下真实文件整理: + +- `sql/init_full_pg17.sql` + +本文档目标: + +- 作为数据库设计说明的统一输出 +- 为开发、测试、运维、交付提供表结构参考 +- 为后续字段扩展、索引优化和数据治理提供基线 + +说明: + +- 本文档优先以当前初始化脚本中的真实表结构为准 +- 若文档内容与历史设计稿不一致,应以 `sql/init_full_pg17.sql` 为最终准绳 + +## 2. 数据库概况 + +当前系统数据库使用 `PostgreSQL 17`,业务表主要分为以下几类: + +- 用户基础信息 +- 客户与商机 +- 拓展管理 +- 工作管理 +- 首页待办与动态 + +当前 CRM 自有表包括: + +- `sys_user` +- `crm_customer` +- `crm_opportunity` +- `crm_opportunity_followup` +- `crm_sales_expansion` +- `crm_channel_expansion` +- `crm_channel_expansion_contact` +- `crm_expansion_followup` +- `work_checkin` +- `work_daily_report` +- `work_daily_report_comment` +- `work_todo` +- `sys_activity_log` + +## 3. 核心关系说明 + +主要业务关系如下: + +```mermaid +erDiagram + SYS_USER ||--o{ CRM_CUSTOMER : owns + SYS_USER ||--o{ CRM_OPPORTUNITY : owns + SYS_USER ||--o{ CRM_SALES_EXPANSION : owns + SYS_USER ||--o{ CRM_CHANNEL_EXPANSION : owns + SYS_USER ||--o{ WORK_CHECKIN : submits + SYS_USER ||--o{ WORK_DAILY_REPORT : submits + SYS_USER ||--o{ WORK_TODO : owns + SYS_USER ||--o{ SYS_ACTIVITY_LOG : operates + + CRM_CUSTOMER ||--o{ CRM_OPPORTUNITY : contains + CRM_OPPORTUNITY ||--o{ CRM_OPPORTUNITY_FOLLOWUP : has + CRM_CHANNEL_EXPANSION ||--o{ CRM_CHANNEL_EXPANSION_CONTACT : has + WORK_DAILY_REPORT ||--o{ WORK_DAILY_REPORT_COMMENT : has +``` + +补充说明: + +- `crm_opportunity` 可关联 `crm_sales_expansion` 和 `crm_channel_expansion` +- `crm_expansion_followup` 通过 `biz_type + biz_id` 关联销售拓展或渠道拓展 +- `work_checkin` 通过 `biz_type + biz_id` 关联销售拓展、渠道拓展或商机 + +## 4. 通用设计约定 + +### 4.1 主键设计 + +所有业务表主键均采用: + +- `bigint generated by default as identity` + +优点: + +- 与 PostgreSQL 原生自增机制兼容 +- 适合单库单体系统场景 + +### 4.2 时间字段 + +大多数业务表统一包含: + +- `created_at` +- `updated_at` + +并通过统一触发器函数 `set_updated_at()` 自动维护更新时间。 + +### 4.3 审计与状态字段 + +多数表包含状态字段,例如: + +- `status` +- `priority` +- `archived` +- `pushed_to_oms` +- `landed_flag` + +这类字段用于支撑页面展示、筛选与流程状态控制。 + +### 4.4 约束设计 + +当前约束主要包括: + +- 主键约束 +- 唯一约束 +- 外键约束 +- `check` 约束 + +用途: + +- 保证状态枚举合法 +- 保证业务编码唯一性 +- 保证级联关系完整 + +## 5. 表设计明细 + +### 5.1 `sys_user` + +表用途: + +- 存储系统用户基础信息 +- 为业务模块提供负责人、提交人、操作人基础身份数据 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 用户主键 | +| `user_code` | varchar(50) | 工号/员工编号 | +| `username` | varchar(50) | 登录账号 | +| `real_name` | varchar(50) | 姓名 | +| `mobile` | varchar(20) | 手机号 | +| `email` | varchar(100) | 邮箱 | +| `org_id` | bigint | 所属组织 ID | +| `job_title` | varchar(100) | 职位 | +| `status` | smallint | 状态,默认 1 | +| `hire_date` | date | 入职日期 | +| `avatar_url` | varchar(255) | 头像地址 | +| `password_hash` | varchar(255) | 密码摘要 | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `uk_sys_user_username` +- `uk_sys_user_mobile` + +索引: + +- `idx_sys_user_org_id` + +### 5.2 `crm_customer` + +表用途: + +- 存储客户主档信息 +- 为商机表提供客户归属 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 客户主键 | +| `customer_code` | varchar(50) | 客户编码 | +| `customer_name` | varchar(200) | 客户名称 | +| `customer_type` | varchar(50) | 客户类型 | +| `industry` | varchar(50) | 所属行业 | +| `province` | varchar(50) | 省份 | +| `city` | varchar(50) | 城市 | +| `address` | varchar(255) | 详细地址 | +| `owner_user_id` | bigint | 当前负责人 | +| `source` | varchar(50) | 客户来源 | +| `status` | varchar(30) | 状态:`potential/following/won/lost` | +| `remark` | text | 备注 | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `uk_crm_customer_code` +- `status check (status in ('potential', 'following', 'won', 'lost'))` + +索引: + +- `idx_crm_customer_owner` +- `idx_crm_customer_name` + +### 5.3 `crm_opportunity` + +表用途: + +- 存储商机主数据 +- 是 CRM 业务的核心主表 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 商机主键 | +| `opportunity_code` | varchar(50) | 商机编号 | +| `opportunity_name` | varchar(200) | 商机名称 | +| `customer_id` | bigint | 客户 ID | +| `owner_user_id` | bigint | 商机负责人 ID | +| `sales_expansion_id` | bigint | 关联销售拓展 ID | +| `channel_expansion_id` | bigint | 关联渠道拓展 ID | +| `pre_sales_id` | bigint | 售前 ID | +| `pre_sales_name` | varchar(100) | 售前姓名 | +| `project_location` | varchar(100) | 项目所在地 | +| `operator_name` | varchar(100) | 运作方 | +| `amount` | numeric(18,2) | 商机金额 | +| `expected_close_date` | date | 预计结单日期 | +| `confidence_pct` | varchar(1) | 把握度:`A/B/C` | +| `stage` | varchar(50) | 商机阶段 | +| `opportunity_type` | varchar(50) | 商机类型 | +| `product_type` | varchar(100) | 产品类型 | +| `source` | varchar(50) | 商机来源 | +| `competitor_name` | varchar(200) | 竞品名称 | +| `archived` | boolean | 是否归档 | +| `pushed_to_oms` | boolean | 是否已推送 OMS | +| `oms_push_time` | timestamptz | 推送时间 | +| `description` | text | 说明/备注 | +| `status` | varchar(30) | 状态:`active/won/lost/closed` | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `uk_crm_opportunity_code` +- `fk_crm_opportunity_customer` +- `fk_crm_opportunity_sales_expansion` +- `fk_crm_opportunity_channel_expansion` +- `confidence_pct check (confidence_pct in ('A', 'B', 'C'))` +- `status check (status in ('active', 'won', 'lost', 'closed'))` + +索引: + +- `idx_crm_opportunity_customer` +- `idx_crm_opportunity_owner` +- `idx_crm_opportunity_sales_expansion` +- `idx_crm_opportunity_channel_expansion` +- `idx_crm_opportunity_stage` +- `idx_crm_opportunity_expected_close` +- `idx_crm_opportunity_archived` + +### 5.4 `crm_opportunity_followup` + +表用途: + +- 存储商机跟进记录 +- 支撑商机详情页时间线与推进历史 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 跟进记录主键 | +| `opportunity_id` | bigint | 商机 ID | +| `followup_time` | timestamptz | 跟进时间 | +| `followup_type` | varchar(50) | 跟进方式 | +| `content` | text | 跟进内容 | +| `next_action` | varchar(255) | 下一步动作 | +| `followup_user_id` | bigint | 跟进人 ID | +| `source_type` | varchar(30) | 来源类型 | +| `source_id` | bigint | 来源记录 ID | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `fk_crm_opportunity_followup_opportunity on delete cascade` + +索引: + +- `idx_crm_opportunity_followup_opportunity_time` +- `idx_crm_opportunity_followup_user` +- `idx_crm_opportunity_followup_source` + +### 5.5 `crm_sales_expansion` + +表用途: + +- 存储销售人员拓展信息 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 销售拓展主键 | +| `employee_no` | varchar(50) | 工号/员工编号 | +| `candidate_name` | varchar(50) | 候选人姓名 | +| `office_name` | varchar(100) | 办事处/代表处 | +| `mobile` | varchar(20) | 手机号 | +| `email` | varchar(100) | 邮箱 | +| `target_dept` | varchar(100) | 所属部门 | +| `industry` | varchar(50) | 所属行业 | +| `title` | varchar(100) | 职务 | +| `intent_level` | varchar(20) | 合作意向:`high/medium/low` | +| `stage` | varchar(50) | 跟进阶段 | +| `has_desktop_exp` | boolean | 是否有云桌面经验 | +| `in_progress` | boolean | 是否持续跟进 | +| `employment_status` | varchar(20) | 状态:`active/left/joined/abandoned` | +| `expected_join_date` | date | 预计入职日期 | +| `owner_user_id` | bigint | 负责人 ID | +| `remark` | text | 备注 | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `uk_crm_sales_expansion_owner_employee_no` +- `intent_level check (intent_level in ('high', 'medium', 'low'))` +- `employment_status check (employment_status in ('active', 'left', 'joined', 'abandoned'))` + +索引: + +- `idx_crm_sales_expansion_owner` +- `idx_crm_sales_expansion_stage` +- `idx_crm_sales_expansion_mobile` + +### 5.6 `crm_channel_expansion` + +表用途: + +- 存储渠道拓展信息 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 渠道拓展主键 | +| `channel_code` | varchar(50) | 渠道编码 | +| `province` | varchar(50) | 省份 | +| `city` | varchar(50) | 城市 | +| `channel_name` | varchar(200) | 渠道名称 | +| `office_address` | varchar(255) | 办公地址 | +| `channel_industry` | varchar(100) | 聚焦行业 | +| `certification_level` | varchar(100) | 认证级别 | +| `annual_revenue` | numeric(18,2) | 年营收 | +| `staff_size` | integer | 人员规模 | +| `contact_established_date` | date | 建立联系日期 | +| `intent_level` | varchar(20) | 合作意向:`high/medium/low` | +| `has_desktop_exp` | boolean | 是否有云桌面经验 | +| `contact_name` | varchar(50) | 主联系人姓名,兼容旧结构 | +| `contact_title` | varchar(100) | 主联系人职务,兼容旧结构 | +| `contact_mobile` | varchar(20) | 主联系人电话,兼容旧结构 | +| `channel_attribute` | varchar(100) | 渠道属性 | +| `internal_attribute` | varchar(100) | 新华三内部属性 | +| `stage` | varchar(50) | 渠道合作阶段 | +| `landed_flag` | boolean | 是否已落地 | +| `expected_sign_date` | date | 预计签约日期 | +| `owner_user_id` | bigint | 负责人 ID | +| `remark` | text | 备注 | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `uk_crm_channel_expansion_code`,仅对非空 `channel_code` 生效 +- `intent_level check (intent_level in ('high', 'medium', 'low'))` +- `staff_size check (staff_size is null or staff_size >= 0)` + +索引: + +- `idx_crm_channel_expansion_owner` +- `idx_crm_channel_expansion_stage` +- `idx_crm_channel_expansion_name` + +### 5.7 `crm_channel_expansion_contact` + +表用途: + +- 存储渠道拓展联系人明细 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 联系人主键 | +| `channel_expansion_id` | bigint | 渠道拓展 ID | +| `contact_name` | varchar(50) | 联系人姓名 | +| `contact_mobile` | varchar(20) | 联系人电话 | +| `contact_title` | varchar(100) | 联系人职务 | +| `sort_order` | integer | 排序号 | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `fk_crm_channel_expansion_contact_channel on delete cascade` + +索引: + +- `idx_crm_channel_expansion_contact_channel` + +### 5.8 `crm_expansion_followup` + +表用途: + +- 存储销售拓展和渠道拓展的统一跟进记录 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 跟进记录主键 | +| `biz_type` | varchar(20) | 业务类型:`sales/channel` | +| `biz_id` | bigint | 业务对象 ID | +| `followup_time` | timestamptz | 跟进时间 | +| `followup_type` | varchar(50) | 跟进方式 | +| `content` | text | 跟进内容 | +| `next_action` | varchar(255) | 下一步动作 | +| `followup_user_id` | bigint | 跟进人 ID | +| `visit_start_time` | timestamptz | 拜访开始时间 | +| `evaluation_content` | text | 评估内容 | +| `next_plan` | text | 后续规划 | +| `source_type` | varchar(30) | 来源类型 | +| `source_id` | bigint | 来源记录 ID | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `biz_type check (biz_type in ('sales', 'channel'))` + +索引: + +- `idx_crm_expansion_followup_biz_time` +- `idx_crm_expansion_followup_user` +- `idx_crm_expansion_followup_source` + +### 5.9 `work_checkin` + +表用途: + +- 存储外勤打卡记录 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 打卡主键 | +| `user_id` | bigint | 打卡人 ID | +| `checkin_date` | date | 打卡日期 | +| `checkin_time` | timestamptz | 打卡时间 | +| `biz_type` | varchar(20) | 关联对象类型:`sales/channel/opportunity` | +| `biz_id` | bigint | 关联对象 ID | +| `biz_name` | varchar(200) | 关联对象名称 | +| `longitude` | numeric(10,6) | 经度 | +| `latitude` | numeric(10,6) | 纬度 | +| `location_text` | varchar(255) | 打卡地点 | +| `remark` | varchar(500) | 备注说明 | +| `user_name` | varchar(100) | 打卡人姓名快照 | +| `dept_name` | varchar(200) | 部门快照 | +| `status` | varchar(30) | 状态:`normal/abnormal/reissue` | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `work_checkin_biz_type_check` +- `status check (status in ('normal', 'abnormal', 'reissue'))` + +索引: + +- `idx_work_checkin_user_date` + +### 5.10 `work_daily_report` + +表用途: + +- 存储销售日报主记录 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 日报主键 | +| `user_id` | bigint | 提交人 ID | +| `report_date` | date | 日报日期 | +| `work_content` | text | 今日工作内容 | +| `tomorrow_plan` | text | 明日工作计划 | +| `source_type` | varchar(30) | 提交来源:`manual/voice` | +| `submit_time` | timestamptz | 提交时间 | +| `status` | varchar(30) | 状态:`draft/submitted/read/reviewed` | +| `score` | integer | 评分,0-100 | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `uk_work_daily_report_user_date` +- `source_type check (source_type in ('manual', 'voice'))` +- `status check (status in ('draft', 'submitted', 'read', 'reviewed'))` +- `score check (score between 0 and 100)` + +索引: + +- `idx_work_daily_report_user_date` +- `idx_work_daily_report_status` + +### 5.11 `work_daily_report_comment` + +表用途: + +- 存储日报点评记录 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 点评主键 | +| `report_id` | bigint | 日报 ID | +| `reviewer_user_id` | bigint | 点评人 ID | +| `score` | integer | 点评分数 | +| `comment_content` | text | 点评内容 | +| `reviewed_at` | timestamptz | 点评时间 | +| `created_at` | timestamptz | 创建时间 | + +关键约束: + +- `fk_work_daily_report_comment_report on delete cascade` +- `score check (score between 0 and 100)` + +索引: + +- `idx_work_daily_report_comment_report` + +### 5.12 `work_todo` + +表用途: + +- 存储首页待办事项 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 待办主键 | +| `user_id` | bigint | 所属用户 ID | +| `title` | varchar(200) | 待办标题 | +| `biz_type` | varchar(30) | 业务类型:`opportunity/expansion/report/other` | +| `biz_id` | bigint | 业务对象 ID | +| `due_date` | timestamptz | 截止时间 | +| `status` | varchar(20) | 状态:`todo/done/canceled` | +| `priority` | varchar(20) | 优先级:`high/medium/low` | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +关键约束: + +- `biz_type check (biz_type in ('opportunity', 'expansion', 'report', 'other'))` +- `status check (status in ('todo', 'done', 'canceled'))` +- `priority check (priority in ('high', 'medium', 'low'))` + +### 5.13 `sys_activity_log` + +表用途: + +- 存储首页最新动态日志 + +主要字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | bigint PK | 动态主键 | +| `biz_type` | varchar(30) | 业务类型 | +| `biz_id` | bigint | 业务对象 ID | +| `action_type` | varchar(50) | 动作类型 | +| `title` | varchar(200) | 动态标题 | +| `content` | varchar(500) | 动态内容 | +| `operator_user_id` | bigint | 操作人 ID | +| `created_at` | timestamptz | 创建时间 | + +索引: + +- `idx_sys_activity_log_created` +- `idx_sys_activity_log_biz` + +## 6. 索引设计汇总 + +当前索引设计主要围绕以下查询场景展开: + +- 客户按负责人、名称查询 +- 商机按客户、负责人、阶段、归档状态、预计结单日查询 +- 商机跟进按商机和时间倒序查询 +- 销售拓展按负责人、手机号查询 +- 渠道拓展按负责人、名称查询 +- 渠道联系人按主表查询 +- 拓展跟进按业务对象和时间倒序查询 +- 打卡和日报按用户、日期倒序查询 +- 动态按时间倒序查询 + +## 7. 触发器与自动维护机制 + +当前脚本中定义了统一触发器: + +- `set_updated_at()` + +并为以下表创建了自动更新时间触发器: + +- `sys_user` +- `crm_customer` +- `crm_opportunity` +- `crm_opportunity_followup` +- `crm_sales_expansion` +- `crm_channel_expansion` +- `crm_channel_expansion_contact` +- `crm_expansion_followup` +- `work_checkin` +- `work_daily_report` +- `work_todo` + +说明: + +- 在表记录更新时,`updated_at` 会自动刷新为当前时间 + +## 8. 兼容性设计说明 + +当前初始化脚本不仅支持全新环境,也兼容旧环境升级,主要处理包括: + +- `sys_user.dept_id` 向 `org_id` 迁移 +- `crm_sales_expansion.target_dept_id` 向 `target_dept` 文本字段迁移 +- `crm_channel_expansion` 补齐 `channel_code`、`city`、`certification_level` 等字段 +- 从旧版 `crm_channel_expansion` 联系人字段迁移到 `crm_channel_expansion_contact` +- `crm_opportunity` 补齐销售拓展、渠道拓展、售前、运作方、竞品等字段 +- `work_checkin` 补齐关联对象与人员快照字段 +- 商机和拓展跟进表补齐来源字段 + +这意味着: + +- 当前数据库设计既考虑了新建环境,也考虑了历史演进连续性 +- 部署时优先执行统一入口脚本,而不是分散执行历史 DDL + +## 9. 数据设计建议 + +结合当前设计,后续建议关注以下方向: + +1. 为外键字段逐步补齐显式外键约束,提升数据一致性。 +2. 对 `work_content`、`tomorrow_plan` 等承载结构化内容的文本字段,明确 JSON 存储规范。 +3. 对高频查询场景继续做慢 SQL 监控与索引优化。 +4. 对商机推送、日报回写等关键动作增加审计表或事件表。 +5. 对历史动态、历史图片和归档商机建立数据归档策略。 + +## 10. 相关文件 + +- `sql/init_full_pg17.sql` +- `docs/business-schema-design.md` +- `docs/system-construction.md` +- `docs/technical-system-planning.md` +- `docs/deployment-guide.md` +