refactor(survey): 优化SOP回执处理逻辑,新增增量保存接口

1. 调整survey_sop_result实体的receiptValue字段类型为TEXT,扩展存储长度
2. 新增SOP回执增量保存的controller和service接口
3. 优化回执详情查询,自动将照片key转换为预签名访问URL
4. 统一处理回执内容的JSON序列化与反序列化
This commit is contained in:
andyesfly committed 2026-09-19 15:12:34 +08:00
1 parent d581fad237
commit 4fe77be1af
3 files changed
+76 -4

No files matched your search

@@ -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);
@@ -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: "回执备注" })
+70 -3
View File
@@ -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<SysRole>;
@Inject()
uploadService!: UploadService;
private async isAdmin(userId: number): Promise<boolean> {
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<any[]> {
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<string>(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<any> {
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<any> {