diff --git a/.trae/specs/rebuild-project-management/checklist.md b/.trae/specs/rebuild-project-management/checklist.md new file mode 100644 index 0000000..0260485 --- /dev/null +++ b/.trae/specs/rebuild-project-management/checklist.md @@ -0,0 +1,31 @@ +# Checklist + +## 后端:安装派单 +- [x] `install_order` entity 字段与 db_design.html D6 完全一致,含注释与索引 +- [x] `POST /api/install-orders` 创建成功并自动生成唯一 install_no,返回统一结构 +- [x] `GET /api/install-orders` 支持关键字/客户/状态/日期范围筛选与分页 +- [x] `PUT /api/install-orders/:id/complete` 仅允许 0→1 流转,重复调用被拒绝 +- [x] 非法入参(缺必填/超长/非法枚举)返回 422 + +## 后端:安全 +- [x] 六个项目管理控制器全部端点:无 JWT 返回 401,无权限点返回 403 +- [x] `sys_menu` 存在 `project:install` 权限点且 RBAC 匹配生效 +- [x] 六个 service 无字符串拼接 SQL,模糊搜索全部绑定参数,排序字段白名单 +- [x] 六个 DTO 对全部外部入参有 class-validator 校验 +- [x] 创建/更新/删除/指派/流转/取消/复核/催办动作均写入 sys_operation_log + +## 前端:页面 +- [x] 勘察台账页含统计卡、状态Tab、高级筛选、台账列、批量指派/催办入口,数据与接口联通 +- [x] 勘察派单页含报价联动、优先级、到场窗口、草稿/派发,提交与接口联通 +- [x] 方案页含设备明细编辑,合同页含金额/支付周期/文件,验收页含关联合同,均为拜访台账式交互 +- [x] 安装派单页 CRUD + 标记完成可用,路由 `/project/install` 与菜单项生效 +- [x] 回访记录页列表/新建/编辑可用 + +## 前端:安全与兼容 +- [x] `views/project/**` 无 `v-html` 渲染后端文本 +- [x] 401 自动跳登录,403/422 有统一提示 +- [x] 所有列表接口响应为 `{ code, message, data: { list, total, page, pageSize } }` + +## 构建 +- [x] `cd client && npx vue-tsc --noEmit` 通过(vue-tsc 无类型错误) +- [x] `cd server && npx tsc --noEmit` 通过(tsc 无类型错误) diff --git a/.trae/specs/rebuild-project-management/spec.md b/.trae/specs/rebuild-project-management/spec.md new file mode 100644 index 0000000..464283b --- /dev/null +++ b/.trae/specs/rebuild-project-management/spec.md @@ -0,0 +1,70 @@ +# 项目管理模块全栈重建 Spec + +## Why + +项目管理模块(勘察派单/方案/合同/安装/验收/回访)后端代码已存在但前端页面未按系统设计稿实现,安装派单(install_order)全栈缺失;同时需要系统性补齐接口鉴权、SQL 注入防护、前端 XSS 防护与操作日志,并保证同一套 API 可被 APP 直接复用。 + +## What Changes + +- **前端重建**:按设计稿重建勘察台账页(参考 `系统设计/UI/项目勘察台帐/code.html`)与勘察派单页(参考 `系统设计/UI/项目勘察派单/code.html`);方案/合同/验收/安装/回访页按客户拜访管理台账(`client/src/views/customer/visit/index.vue`)的交互与样式体系重建。 +- **安装派单全栈新增**:install_order 的 entity / dto / service / controller / 前端台账页 / API / 路由,功能深度为台账式完整 CRUD + 状态流转(0已派发 → 1已完成)。 +- **安全加固**: + - 接口鉴权补全:项目管理所有接口统一走 JWT 认证 + RBAC 权限点(复用现有 `RbacMiddleware`,按 path 匹配 permission code)。 + - SQL 注入防护审查:全部查询走 Sequelize 参数化,禁止字符串拼接 SQL;DTO 层用 class-validator 全量校验入参。 + - 前端 XSS 防护:用户输入的备注/说明/意见等文本字段渲染时禁止 `v-html`,统一文本插值;富文本场景如需 HTML 必须经白名单消毒。 + - 操作日志埋点:创建/更新/删除/指派/状态流转/取消/复核等关键动作写入 `sys_operation_log`。 +- **APP 兼容**:不新增命名空间,Web 与 APP 共用现有 `/api/project-*` 系列接口;统一响应结构 `{ code, message, data }`,分页结构统一为 `{ list, total, page, pageSize }`;字段命名与时间格式(ISO 8601)保持稳定。 + +## Impact + +- Affected specs: 项目管理(新建)、系统安全(加固) +- Affected code: + - server:`src/entity/install_order.entity.ts`(新增)、`src/dto/install_order.dto.ts`(新增)、`src/service/install_order.service.ts`(新增)、`src/controller/install_order.controller.ts`(新增)、既有 project_plan / contract / survey_order / acceptance / visit_record 的 controller/service(安全审查与日志埋点) + - client:`src/views/project/**`(全部重建)、`src/api/installOrder.ts`(新增)、`src/router/index.ts`(补安装派单路由)、`src/layouts/index.vue`(菜单补项) + +## ADDED Requirements + +### Requirement: 安装派单管理(install_order 全栈) +系统 SHALL 提供安装派单的台账式完整 CRUD 与状态流转:列表分页筛选(关键字/客户/状态/派单日期范围)、新建、编辑、详情、删除、标记完成。单号自动生成(格式与勘察单号规则一致,如 `WH-INST-YYYY-NNNN`)。 + +#### Scenario: 创建安装派单 +- **WHEN** 授权用户提交合法派单表单(customerId、planId、installer、dispatchDate 必填) +- **THEN** 系统生成唯一 install_no,状态为 0(已派发),写入操作日志,返回统一成功响应 + +#### Scenario: 状态流转 +- **WHEN** 授权用户对已派发工单执行「标记完成」 +- **THEN** 状态从 0 流转为 1,记录完成时间,写入操作日志;已完成工单禁止再次流转 + +### Requirement: 勘察台账页按设计稿重建 +系统 SHALL 按 `系统设计/UI/项目勘察台帐/code.html` 重建勘察台账页:统计卡片(待指派/勘察中等待复核/已归档)、状态筛选 Tab、高级筛选(优先级/业务员/电工/时间窗口)、列表列(工单编号+优先级、关联客户与项目、空间位置、测绘回路规格、责任业务员、指派电工、期望时效/SLA、预估劳务费、状态、操作)、批量指派、催办、智能派单入口。 + +#### Scenario: 台账筛选 +- **WHEN** 用户组合使用状态 Tab 与高级筛选条件 +- **THEN** 列表与统计卡片按筛选结果刷新,分页保持统一响应结构 + +### Requirement: 勘察派单页按设计稿重建 +系统 SHALL 按 `系统设计/UI/项目勘察派单/code.html` 重建派单表单页:关联客户选择、任务标题、空间位置、回路规格/数量、优先级(特急P1/加急P2/常规P3)、计划到场窗口、SLA、预估劳务费自动报价(复用 `/api/survey-orders/quote`)、指派电工、草稿保存与正式派发。 + +### Requirement: 方案/合同/验收/回访页重建 +系统 SHALL 按客户拜访台账的交互体系(筛选区 + 表格 + 分页 + 新建/编辑弹窗或表单页 + 状态标签)重建项目方案(含设备明细)、合同(含金额/支付周期/合同文件)、项目验收(含验收意见/文件)、回访记录四个台账页。 + +### Requirement: 接口鉴权补全 +系统 SHALL 保证 `/api/survey-orders`、`/api/project-plans`、`/api/contracts`、`/api/install-orders`、`/api/acceptances`、`/api/visit-records` 全部端点强制 JWT 认证并校验对应 RBAC 权限点(`project:survey`、`project:plan`、`project:contract`、`project:install`、`project:acceptance`、`project:visit`)。 + +#### Scenario: 未认证访问被拒 +- **WHEN** 请求未携带有效 JWT +- **THEN** 返回 401 统一错误结构;无权限点返回 403 + +### Requirement: SQL 注入防护审查 +系统 SHALL 保证项目管理模块所有数据库访问经由 Sequelize 参数化;关键字模糊搜索使用绑定参数的 `Op.like`,排序字段白名单校验,禁止任何字符串拼接 SQL。 + +### Requirement: 前端 XSS 防护 +系统 SHALL 禁止对后端返回的用户输入文本使用 `v-html`;表格/详情中的备注、说明、意见字段使用文本插值渲染。 + +### Requirement: 操作日志埋点 +系统 SHALL 在创建/更新/删除/指派/批量指派/状态流转/取消/复核/催办动作中写入 `sys_operation_log`(操作人、动作、目标单号、摘要)。 + +## MODIFIED Requirements + +### Requirement: 统一响应结构(APP 兼容) +项目管理全部接口 SHALL 返回 `{ code, message, data }`;列表 data 统一为 `{ list, total, page, pageSize }`;时间字段序列化为 ISO 8601 字符串;错误码语义稳定(401 未认证 / 403 无权限 / 422 参数校验失败 / 500 服务器错误),供 APP 端直接消费。 diff --git a/.trae/specs/rebuild-project-management/tasks.md b/.trae/specs/rebuild-project-management/tasks.md new file mode 100644 index 0000000..c5c854c --- /dev/null +++ b/.trae/specs/rebuild-project-management/tasks.md @@ -0,0 +1,37 @@ +# Tasks + +## 后端:安装派单(新增) +- [x] Task 1: 新增 install_order 后端全栈 + - [x] 1.1 创建 `src/entity/install_order.entity.ts`(字段对齐 db_design.html D6:install_no/customer_id/installer/worker_id/dispatch_date/start_date/end_date/plan_id/remark/status/created_at) + - [x] 1.2 创建 `src/dto/install_order.dto.ts`(class-validator:QueryDTO/CreateDTO/UpdateDTO,必填校验、长度限制、状态枚举校验) + - [x] 1.3 创建 `src/service/install_order.service.ts`(list 分页+筛选、detail、create 生成单号、update、delete、complete 状态流转;全程参数化查询) + - [x] 1.4 创建 `src/controller/install_order.controller.ts`(`/api/install-orders`:GET /、GET /:id、POST /、PUT /:id、PUT /:id/complete、DELETE /:id) + +## 后端:安全加固(可并行) +- [x] Task 2: 接口鉴权补全 + - [x] 2.1 核对六个项目管理控制器的 JWT + RbacMiddleware 挂载,确认 path→permission code 映射覆盖(project:survey/plan/contract/install/acceptance/visit) + - [x] 2.2 在 `sys_menu` 种子/迁移中补齐 `project:install` 权限点 +- [x] Task 3: SQL 注入防护审查 + - [x] 3.1 审查六个 service,确认无字符串拼接 SQL、模糊搜索均为绑定参数 `Op.like`、排序字段白名单 + - [x] 3.2 审查六个 DTO,确认所有外部入参均有 class-validator 校验(类型/长度/枚举/范围) +- [x] Task 4: 操作日志埋点 + - [x] 4.1 封装/复用操作日志写入方法(写 sys_operation_log:操作人、模块、动作、目标单号、摘要) + - [x] 4.2 在六个 service 的 create/update/delete/assign/batchAssign/状态流转/cancel/review/urge/complete 处埋点 + +## 前端:页面重建 +- [x] Task 5: 重建勘察台账页 `views/project/survey/index.vue`(对齐 `系统设计/UI/项目勘察台帐/code.html`:统计卡、状态Tab、高级筛选、台账列、批量指派/催办/智能派单入口) +- [x] Task 6: 重建勘察派单页 `views/project/survey/form.vue`(对齐 `系统设计/UI/项目勘察派单/code.html`:客户选择、回路规格/数量、优先级、到场窗口、SLA、劳务费报价联动、草稿/派发) +- [x] Task 7: 重建方案管理页 `views/project/plan/index.vue`(拜访台账式:筛选+表格+分页+新建/编辑含设备明细+状态标签) +- [x] Task 8: 重建合同管理页 `views/project/contract/index.vue`(筛选+表格+新建/编辑含金额/支付周期/合同文件上传+状态标签) +- [x] Task 9: 重建项目验收页 `views/project/acceptance/index.vue`(筛选+表格+新建/编辑含验收意见/文件+关联合同) +- [x] Task 10: 新增安装派单页 `views/project/install/index.vue` + `src/api/installOrder.ts` + 路由 `/project/install` + 布局菜单项(台账式 CRUD + 标记完成) +- [x] Task 11: 重建回访记录页 `views/project/visit/index.vue` + 补齐 `src/api/visitRecord.ts`(如缺失) +- [x] Task 12: 前端 XSS 防护审查 + - [x] 12.1 全量检查 `views/project/**` 无 `v-html` 渲染后端文本;备注/说明/意见统一文本插值 + - [x] 12.2 确认 axios 拦截器对 401 跳登录、403/422 统一提示,响应结构与 APP 兼容约定一致 + +## Task Dependencies +- Task 5、6 依赖后端 survey_order 既有接口(不阻塞,可与 Task 1-4 并行) +- Task 10 依赖 Task 1(安装派单后端先行) +- Task 12 依赖 Task 5-11(在页面重建完成后统一审查) +- Task 4 依赖 Task 1(install service 一并埋点) diff --git a/client/src/api/acceptance.ts b/client/src/api/acceptance.ts index 212b18a..619d0dd 100644 --- a/client/src/api/acceptance.ts +++ b/client/src/api/acceptance.ts @@ -18,6 +18,8 @@ export interface AcceptanceQuery { pageSize?: number; keyword?: string; customerId?: number; + acceptStartDate?: string; + acceptEndDate?: string; } export interface AcceptanceListResult { diff --git a/client/src/api/contract.ts b/client/src/api/contract.ts index b9ceac2..820820c 100644 --- a/client/src/api/contract.ts +++ b/client/src/api/contract.ts @@ -2,11 +2,13 @@ import request from '@/utils/request'; export interface ContractInfo { id: number; + contractNo: string; customerId: number; xtContractNo?: string; contractAmount?: number; payCycle?: string; salesmanId?: number; + salesmanName?: string; signDate?: string; contractDesc?: string; startDate?: string; @@ -24,6 +26,8 @@ export interface ContractQuery { customerId?: number; salesmanId?: number; status?: number; + signStartDate?: string; + signEndDate?: string; } export interface ContractListResult { diff --git a/client/src/api/installOrder.ts b/client/src/api/installOrder.ts new file mode 100644 index 0000000..8d7534a --- /dev/null +++ b/client/src/api/installOrder.ts @@ -0,0 +1,112 @@ +import request from '@/utils/request'; + +// 安装派单状态:0已派发 1已完成 +export const INSTALL_STATUS_OPTIONS = [ + { value: 0, label: '已派发' }, + { value: 1, label: '已完成' } +]; + +export const INSTALL_STATUS = { + DISPATCHED: 0, + COMPLETED: 1 +}; + +// 关联客户精简字段(列表 decorate 附带) +export interface InstallOrderCustomer { + id: number; + customerNo?: string; + customerName?: string; + contactPerson?: string; + contactPhone?: string; + address?: string; +} + +// 共享电工精简字段 +export interface InstallOrderWorker { + id: number; + workerName?: string; + phone?: string; + workerTitle?: string; +} + +// 关联方案精简字段 +export interface InstallOrderPlan { + id: number; + planNo?: string; + planDesc?: string; + status?: number; +} + +export interface InstallOrderInfo { + id: number; + installNo: string; + customerId: number; + installer?: string; + workerId?: number; + dispatchDate?: string; + startDate?: string; + endDate?: string; + planId: number; + remark?: string; + status: number; + created_at?: string; + updated_at?: string; + // 关联信息(列表/详情 decorate 附带) + customer?: InstallOrderCustomer; + worker?: InstallOrderWorker; + plan?: InstallOrderPlan; + customerName?: string; + workerName?: string; + planNo?: string; +} + +export interface InstallOrderQuery { + page?: number; + pageSize?: number; + keyword?: string; + customerId?: number; + status?: number; + // 派单日期范围,逗号分隔字符串 [startDate,endDate],后端 split 解析 + dateRange?: string; +} + +export interface InstallOrderListResult { + list: InstallOrderInfo[]; + total: number; + page: number; + pageSize: number; +} + +export interface InstallOrderCreateParams { + customerId: number; + installer?: string; + workerId?: number; + dispatchDate?: string; + startDate?: string; + endDate?: string; + planId: number; + remark?: string; + status?: number; +} + +export type InstallOrderUpdateParams = Partial; + +export const installOrderApi = { + getList: (params: InstallOrderQuery) => + request.get('/install-orders', { params }), + + getDetail: (id: number) => + request.get(`/install-orders/${id}`), + + create: (data: InstallOrderCreateParams) => + request.post('/install-orders', data), + + update: (id: number, data: InstallOrderUpdateParams) => + request.put(`/install-orders/${id}`, data), + + // 标记完成:0已派发 → 1已完成 + complete: (id: number) => + request.put(`/install-orders/${id}/complete`), + + delete: (id: number) => request.delete(`/install-orders/${id}`) +}; diff --git a/client/src/api/projectPlan.ts b/client/src/api/projectPlan.ts index 6d11002..5572dde 100644 --- a/client/src/api/projectPlan.ts +++ b/client/src/api/projectPlan.ts @@ -29,6 +29,8 @@ export interface ProjectPlanQuery { keyword?: string; customerId?: number; status?: number; + registerStartDate?: string; + registerEndDate?: string; } export interface ProjectPlanListResult { diff --git a/client/src/api/upload.ts b/client/src/api/upload.ts index 5841e4b..2aa772b 100644 --- a/client/src/api/upload.ts +++ b/client/src/api/upload.ts @@ -9,6 +9,13 @@ export interface UploadImageResult { size: number; } +export interface UploadFileResult { + key: string; + url: string; + name: string; + size: number; +} + // 上传现场照片到对象存储(RustFS),返回对象 key 与预签名 URL export const uploadApi = { uploadImage: (file: File) => { @@ -19,5 +26,15 @@ export const uploadApi = { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 60000 }); + }, + + // 上传方案/合同/验收等业务文件到对象存储,返回对象 key 与预签名 URL + uploadFile: (file: File) => { + const formData = new FormData(); + formData.append('files', file); + return request.post('/upload/file', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 60000 + }); } }; diff --git a/client/src/api/visitRecord.ts b/client/src/api/visitRecord.ts index d03493a..b6b6860 100644 --- a/client/src/api/visitRecord.ts +++ b/client/src/api/visitRecord.ts @@ -2,6 +2,7 @@ import request from '@/utils/request'; export interface VisitRecordInfo { id: number; + visitNo?: string; customerId: number; contactPerson?: string; contactPhone?: string; @@ -17,6 +18,8 @@ export interface VisitRecordQuery { pageSize?: number; keyword?: string; customerId?: number; + visitStartDate?: string; + visitEndDate?: string; } export interface VisitRecordListResult { diff --git a/client/src/router/index.ts b/client/src/router/index.ts index e64ef4d..e2856cb 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -145,6 +145,12 @@ const routes: RouteRecordRaw[] = [ component: () => import("@/views/project/visit/index.vue"), meta: { requiresAuth: true, title: "回访记录", permission: "project:visit" } }, + { + path: "project/install", + name: "ProjectInstall", + component: () => import("@/views/project/install/index.vue"), + meta: { requiresAuth: true, title: "安装派单", permission: "project:install" } + }, { path: "dispatch", name: "Dispatch", diff --git a/client/src/views/project/acceptance/index.vue b/client/src/views/project/acceptance/index.vue index 7fd2826..1d9aeee 100644 --- a/client/src/views/project/acceptance/index.vue +++ b/client/src/views/project/acceptance/index.vue @@ -1,80 +1,184 @@