diff --git a/server/src/controller/survey_order.controller.ts b/server/src/controller/survey_order.controller.ts index 39b5bff..6433b78 100644 --- a/server/src/controller/survey_order.controller.ts +++ b/server/src/controller/survey_order.controller.ts @@ -23,6 +23,11 @@ export class SurveyOrderController { @Post("/") async create(@Body() body: any) { return this.surveyOrderService.create(body, this.ctx); } @Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.surveyOrderService.update(Number(id), body, this.ctx); } + // 现场回执:按 SOP 检查项增量录入(照片/截图 key 数组、表单值、备注) + @Put("/:id/sop-results") async saveSopResults(@Param("id") id: string, @Body() body: any) { + return this.surveyOrderService.saveSopResults(Number(id), body, this.ctx); + } + // 指派 / 改派共享电工 @Put("/:id/assign") async assign(@Param("id") id: string, @Body() body: any) { return this.surveyOrderService.assign(Number(id), body, this.ctx); diff --git a/server/src/entity/survey_sop_result.entity.ts b/server/src/entity/survey_sop_result.entity.ts index 85d6bfa..adcb689 100644 --- a/server/src/entity/survey_sop_result.entity.ts +++ b/server/src/entity/survey_sop_result.entity.ts @@ -28,7 +28,7 @@ export default class SurveySopResult extends Model { @Column({ type: DataType.BOOLEAN, allowNull: false, defaultValue: false, comment: "是否已完成回执" }) checked!: boolean; - @Column({ type: DataType.STRING(1024), field: "receipt_value", comment: "回执内容(照片对象key数组/表单值/截图key)" }) + @Column({ type: DataType.TEXT, field: "receipt_value", comment: "回执内容(照片对象key的JSON数组字符串/表单文本值/测仪截图key的JSON数组字符串)" }) receiptValue!: string; @Column({ type: DataType.STRING(255), comment: "回执备注" }) diff --git a/server/src/service/survey_order.service.ts b/server/src/service/survey_order.service.ts index 1bc06d9..dd2944a 100644 --- a/server/src/service/survey_order.service.ts +++ b/server/src/service/survey_order.service.ts @@ -1,4 +1,4 @@ -import { Provide } from "@midwayjs/core"; +import { Provide, Inject } from "@midwayjs/core"; import { InjectRepository } from "@midwayjs/sequelize"; import { Repository } from "sequelize-typescript"; import { Op, UniqueConstraintError, literal } from "sequelize"; @@ -12,6 +12,7 @@ import DispatchPrice from "../entity/dispatch_price.entity"; import SysUser from "../entity/sysUser.entity"; import SysUserRole from "../entity/sysUserRole.entity"; import SysRole from "../entity/sysRole.entity"; +import { UploadService } from "./upload.service"; // 勘察工单状态:0待指派 1现场勘察中 2待技术复核 3已归档 4预留 5已取消 const STATUS = { PENDING_ASSIGN: 0, SURVEYING: 1, PENDING_REVIEW: 2, ARCHIVED: 3, RESERVED: 4, CANCELED: 5 }; @@ -53,6 +54,9 @@ export class SurveyOrderService { @InjectRepository(SysRole) roleModel!: Repository; + @Inject() + uploadService!: UploadService; + private async isAdmin(userId: number): Promise { const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] }); return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN"); @@ -351,21 +355,27 @@ export class SurveyOrderService { const vo = this.toVO(record); await this.decorate([vo]); vo.sopResults = await this.getSopResults(id); + // 现场取证照片:把对象 key 批量换取预签名访问 URL,供详情只读视图直接渲染 + const photoKeys: string[] = vo.photos || []; + const urlMap = photoKeys.length ? await this.uploadService.getPresignedUrlMap(photoKeys) : {}; + vo.photoUrls = photoKeys.map((k) => urlMap[k] || ""); return { code: 200, message: "获取勘察派单详情成功", data: vo }; } - // 工单回执 + 检查项信息(手动 join,避免额外关联) + // 工单回执 + 检查项信息(手动 join,避免额外关联);receiptValue 以 key 数组出参,并补预签名访问 URL private async getSopResults(orderId: number): Promise { const results = await this.sopResultModel.findAll({ where: { surveyOrderId: orderId }, order: [["id", "ASC"]] }); if (results.length === 0) return []; const itemIds = [...new Set(results.map((r) => r.sopItemId).filter(Boolean))]; const items = await this.sopItemModel.findAll({ where: { id: { [Op.in]: itemIds } } }); const itemMap = new Map(items.map((i) => [i.id, i])); - return results.map((r) => { + + const rows = results.map((r) => { const json: any = r.toJSON(); const item: any = itemMap.get(json.sopItemId); return { ...json, + receiptValue: this.parsePhotos(json.receiptValue), itemName: item?.itemName || "", itemDesc: item?.itemDesc || "", receiptType: item?.receiptType || "", @@ -373,6 +383,63 @@ export class SurveyOrderService { required: item?.required ?? false }; }); + + // photo/meter 类回执是照片 key,批量换取预签名 URL 供前端直接渲染;form 类是文本值,不参与换取 + const keys = [...new Set(rows.filter((r) => r.receiptType !== "form").flatMap((r) => r.receiptValue))]; + const urlMap = keys.length ? await this.uploadService.getPresignedUrlMap(keys) : {}; + return rows.map((row) => ({ + ...row, + receiptUrls: row.receiptType === "form" ? [] : row.receiptValue.map((k: string) => urlMap[k] || "") + })); + } + + // 保存 SOP 工单回执(增量:只更新传入的检查项,未传入项保持原值) + async saveSopResults(id: number, dto: any, ctx: Context): Promise { + const record = await this.surveyModel.findByPk(id); + if (!record) return { code: 404, message: "勘察派单不存在", data: null }; + if (!(await this.isOrderVisible(ctx.state.user.userId, record))) return { code: 403, message: "无权操作该工单", data: null }; + if (record.status !== STATUS.SURVEYING) return { code: 400, message: "仅现场勘察中的工单可录入回执", data: null }; + + const incoming: any[] = Array.isArray(dto?.results) ? dto.results : []; + if (incoming.length === 0) return { code: 400, message: "回执内容不能为空", data: null }; + + // 仅允许更新本工单已生成的回执记录,防止跨工单/跨检查项写入 + const existing = await this.sopResultModel.findAll({ where: { surveyOrderId: id } }); + const resultMap = new Map(existing.map((r) => [Number(r.sopItemId), r])); + + let updated = 0; + const skipped: number[] = []; + for (const item of incoming) { + const sopItemId = Number(item?.sopItemId); + const target = resultMap.get(sopItemId); + if (!target) { + skipped.push(sopItemId); + continue; + } + + const patch: any = {}; + if (item.checked !== undefined) { + const checked = !!item.checked; + patch.checked = checked; + // 首次标记完成时记录回执时间,回退未完成则清空 + patch.checkedAt = checked ? target.checkedAt || new Date() : null; + } + if (item.receiptValue !== undefined) { + const keys = this.parsePhotos(item.receiptValue); + patch.receiptValue = keys.length ? JSON.stringify(keys) : null; + } + if (item.remark !== undefined) patch.remark = item.remark || null; + if (Object.keys(patch).length === 0) continue; + + await target.update(patch); + updated++; + } + + return { + code: 200, + message: "保存勘察回执成功", + data: { updated, skipped, sopResults: await this.getSopResults(id) } + }; } async create(data: any, ctx: Context): Promise {