refactor(survey): 优化SOP回执处理逻辑,新增增量保存接口
1. 调整survey_sop_result实体的receiptValue字段类型为TEXT,扩展存储长度 2. 新增SOP回执增量保存的controller和service接口 3. 优化回执详情查询,自动将照片key转换为预签名访问URL 4. 统一处理回执内容的JSON序列化与反序列化
This commit is contained in:
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); }
|
@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); }
|
@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) {
|
@Put("/:id/assign") async assign(@Param("id") id: string, @Body() body: any) {
|
||||||
return this.surveyOrderService.assign(Number(id), body, this.ctx);
|
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: "是否已完成回执" })
|
@Column({ type: DataType.BOOLEAN, allowNull: false, defaultValue: false, comment: "是否已完成回执" })
|
||||||
checked!: boolean;
|
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;
|
receiptValue!: string;
|
||||||
|
|
||||||
@Column({ type: DataType.STRING(255), comment: "回执备注" })
|
@Column({ type: DataType.STRING(255), comment: "回执备注" })
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Provide } from "@midwayjs/core";
|
import { Provide, Inject } from "@midwayjs/core";
|
||||||
import { InjectRepository } from "@midwayjs/sequelize";
|
import { InjectRepository } from "@midwayjs/sequelize";
|
||||||
import { Repository } from "sequelize-typescript";
|
import { Repository } from "sequelize-typescript";
|
||||||
import { Op, UniqueConstraintError, literal } from "sequelize";
|
import { Op, UniqueConstraintError, literal } from "sequelize";
|
||||||
@@ -12,6 +12,7 @@ import DispatchPrice from "../entity/dispatch_price.entity";
|
|||||||
import SysUser from "../entity/sysUser.entity";
|
import SysUser from "../entity/sysUser.entity";
|
||||||
import SysUserRole from "../entity/sysUserRole.entity";
|
import SysUserRole from "../entity/sysUserRole.entity";
|
||||||
import SysRole from "../entity/sysRole.entity";
|
import SysRole from "../entity/sysRole.entity";
|
||||||
|
import { UploadService } from "./upload.service";
|
||||||
|
|
||||||
// 勘察工单状态:0待指派 1现场勘察中 2待技术复核 3已归档 4预留 5已取消
|
// 勘察工单状态:0待指派 1现场勘察中 2待技术复核 3已归档 4预留 5已取消
|
||||||
const STATUS = { PENDING_ASSIGN: 0, SURVEYING: 1, PENDING_REVIEW: 2, ARCHIVED: 3, RESERVED: 4, CANCELED: 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)
|
@InjectRepository(SysRole)
|
||||||
roleModel!: Repository<SysRole>;
|
roleModel!: Repository<SysRole>;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
uploadService!: UploadService;
|
||||||
|
|
||||||
private async isAdmin(userId: number): Promise<boolean> {
|
private async isAdmin(userId: number): Promise<boolean> {
|
||||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||||
@@ -351,21 +355,27 @@ export class SurveyOrderService {
|
|||||||
const vo = this.toVO(record);
|
const vo = this.toVO(record);
|
||||||
await this.decorate([vo]);
|
await this.decorate([vo]);
|
||||||
vo.sopResults = await this.getSopResults(id);
|
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 };
|
return { code: 200, message: "获取勘察派单详情成功", data: vo };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 工单回执 + 检查项信息(手动 join,避免额外关联)
|
// 工单回执 + 检查项信息(手动 join,避免额外关联);receiptValue 以 key 数组出参,并补预签名访问 URL
|
||||||
private async getSopResults(orderId: number): Promise<any[]> {
|
private async getSopResults(orderId: number): Promise<any[]> {
|
||||||
const results = await this.sopResultModel.findAll({ where: { surveyOrderId: orderId }, order: [["id", "ASC"]] });
|
const results = await this.sopResultModel.findAll({ where: { surveyOrderId: orderId }, order: [["id", "ASC"]] });
|
||||||
if (results.length === 0) return [];
|
if (results.length === 0) return [];
|
||||||
const itemIds = [...new Set(results.map((r) => r.sopItemId).filter(Boolean))];
|
const itemIds = [...new Set(results.map((r) => r.sopItemId).filter(Boolean))];
|
||||||
const items = await this.sopItemModel.findAll({ where: { id: { [Op.in]: itemIds } } });
|
const items = await this.sopItemModel.findAll({ where: { id: { [Op.in]: itemIds } } });
|
||||||
const itemMap = new Map(items.map((i) => [i.id, i]));
|
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 json: any = r.toJSON();
|
||||||
const item: any = itemMap.get(json.sopItemId);
|
const item: any = itemMap.get(json.sopItemId);
|
||||||
return {
|
return {
|
||||||
...json,
|
...json,
|
||||||
|
receiptValue: this.parsePhotos(json.receiptValue),
|
||||||
itemName: item?.itemName || "",
|
itemName: item?.itemName || "",
|
||||||
itemDesc: item?.itemDesc || "",
|
itemDesc: item?.itemDesc || "",
|
||||||
receiptType: item?.receiptType || "",
|
receiptType: item?.receiptType || "",
|
||||||
@@ -373,6 +383,63 @@ export class SurveyOrderService {
|
|||||||
required: item?.required ?? false
|
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> {
|
async create(data: any, ctx: Context): Promise<any> {
|
||||||
|
|||||||
Reference in new issue
Block a user