新增通知规则实体、扫描任务、API与管理界面,重构站内通知支持用户/电工双类型,移除硬编码的证照到期提醒,新增工单派单、电工审核结果等通知触发场景,调整菜单与数据库结构适配新通知体系。
323 lines
13 KiB
TypeScript
323 lines
13 KiB
TypeScript
import { Provide, Inject } from "@midwayjs/core";
|
||
import { InjectRepository } from "@midwayjs/sequelize";
|
||
import { Repository } from "sequelize-typescript";
|
||
import { Op, UniqueConstraintError } from "sequelize";
|
||
import { Context } from "@midwayjs/koa";
|
||
import MaintenanceOrder from "../entity/maintenance_order.entity";
|
||
import MaintenanceDetail from "../entity/maintenance_detail.entity";
|
||
import Customer from "../entity/customer.entity";
|
||
import Worker from "../entity/worker.entity";
|
||
import SysUserRole from "../entity/sysUserRole.entity";
|
||
import SysRole from "../entity/sysRole.entity";
|
||
import { LogService } from "./log.service";
|
||
import { NoticeRuleService } from "./notice_rule.service";
|
||
|
||
// 维保派单状态:0已派发 1已完成
|
||
const STATUS = { DISPATCHED: 0, COMPLETED: 1 };
|
||
|
||
@Provide()
|
||
export class MaintenanceService {
|
||
@InjectRepository(MaintenanceOrder)
|
||
orderModel!: Repository<MaintenanceOrder>;
|
||
|
||
@InjectRepository(MaintenanceDetail)
|
||
detailModel!: Repository<MaintenanceDetail>;
|
||
|
||
@InjectRepository(Customer)
|
||
customerModel!: Repository<Customer>;
|
||
|
||
@InjectRepository(Worker)
|
||
workerModel!: Repository<Worker>;
|
||
|
||
@InjectRepository(SysUserRole)
|
||
userRoleModel!: Repository<SysUserRole>;
|
||
|
||
@InjectRepository(SysRole)
|
||
roleModel!: Repository<SysRole>;
|
||
|
||
@Inject()
|
||
logService!: LogService;
|
||
|
||
@Inject()
|
||
noticeRuleService!: NoticeRuleService;
|
||
|
||
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");
|
||
}
|
||
|
||
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
|
||
if (await this.isAdmin(userId)) return true;
|
||
const customer = await this.customerModel.findByPk(customerId);
|
||
return !!customer && customer.salesmanId === userId;
|
||
}
|
||
|
||
// 工单可见性:管理员 / 客户归属业务员
|
||
private async isOrderVisible(userId: number, order: MaintenanceOrder): Promise<boolean> {
|
||
if (await this.isAdmin(userId)) return true;
|
||
return this.isCustomerOwner(userId, order.customerId);
|
||
}
|
||
|
||
// query 参数 → Sequelize where(关键字/客户/电工/状态/维保日期范围)
|
||
private buildWhere(query: any): any {
|
||
const where: any = {};
|
||
if (query.keyword) {
|
||
where[Op.or] = [
|
||
{ maintenanceNo: { [Op.like]: `%${query.keyword}%` } },
|
||
{ maintainer: { [Op.like]: `%${query.keyword}%` } },
|
||
{ remark: { [Op.like]: `%${query.keyword}%` } }
|
||
];
|
||
}
|
||
if (query.customerId !== undefined && query.customerId !== "") where.customerId = Number(query.customerId);
|
||
if (query.workerId !== undefined && query.workerId !== "") where.workerId = Number(query.workerId);
|
||
if (query.status !== undefined && query.status !== "") where.status = Number(query.status);
|
||
// maintenance_date 范围 [startTime, endTime],YYYY-MM-DD
|
||
if (query.startTime || query.endTime) {
|
||
const cond: any = {};
|
||
if (query.startTime) cond[Op.gte] = query.startTime;
|
||
if (query.endTime) cond[Op.lte] = query.endTime;
|
||
where.maintenanceDate = cond;
|
||
}
|
||
return where;
|
||
}
|
||
|
||
// 行级隔离:非管理员仅可见「客户归属自己」的工单
|
||
private async scopeWhere(baseWhere: any, ctx: Context): Promise<any> {
|
||
const userId = ctx.state.user.userId;
|
||
const where: any = { ...baseWhere };
|
||
if (await this.isAdmin(userId)) return where;
|
||
|
||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||
const ids = myCustomers.map((c) => c.id);
|
||
where.customerId = ids.length ? { [Op.in]: ids } : -1;
|
||
return where;
|
||
}
|
||
|
||
// 批量补充客户/电工信息,避免 N+1
|
||
private async decorate(rows: any[]): Promise<any[]> {
|
||
if (rows.length === 0) return rows;
|
||
const customerIds = [...new Set(rows.map((r) => r.customerId).filter(Boolean))];
|
||
const workerIds = [...new Set(rows.map((r) => r.workerId).filter(Boolean))];
|
||
|
||
const [customers, workers] = await Promise.all([
|
||
customerIds.length
|
||
? this.customerModel.findAll({
|
||
where: { id: { [Op.in]: customerIds } },
|
||
attributes: ["id", "customerNo", "customerName", "contactPerson", "contactPhone", "address"]
|
||
})
|
||
: Promise.resolve([]),
|
||
workerIds.length
|
||
? this.workerModel.findAll({
|
||
where: { id: { [Op.in]: workerIds } },
|
||
attributes: ["id", "workerName", "phone", "workerTitle"]
|
||
})
|
||
: Promise.resolve([])
|
||
]);
|
||
|
||
const customerMap = new Map(customers.map((c) => [c.id, c]));
|
||
const workerMap = new Map(workers.map((w) => [w.id, w]));
|
||
|
||
for (const row of rows) {
|
||
const customer: any = customerMap.get(row.customerId);
|
||
const worker: any = workerMap.get(row.workerId);
|
||
row.customer = customer ? customer.toJSON() : null;
|
||
row.customerName = customer?.customerName || "";
|
||
row.worker = worker ? worker.toJSON() : null;
|
||
row.workerName = worker?.workerName || "";
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
// 统一清洗入参:空串转 null
|
||
private normalizePayload(data: any): any {
|
||
const payload: any = { ...data };
|
||
for (const key of Object.keys(payload)) {
|
||
if (payload[key] === "") payload[key] = null;
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
async listOrders(query: any, ctx: Context): Promise<any> {
|
||
const page = Number(query.page) || 1;
|
||
const pageSize = Number(query.pageSize) || 10;
|
||
|
||
const where = await this.scopeWhere(this.buildWhere(query), ctx);
|
||
const { count, rows } = await this.orderModel.findAndCountAll({
|
||
where,
|
||
limit: pageSize,
|
||
offset: (page - 1) * pageSize,
|
||
order: [["id", "DESC"]],
|
||
distinct: true
|
||
});
|
||
|
||
const list = await this.decorate(rows.map((r) => r.toJSON()));
|
||
return { code: 200, message: "获取维保派单列表成功", data: { list, total: count, page, pageSize } };
|
||
}
|
||
|
||
async getOrderDetail(id: number, ctx: Context): Promise<any> {
|
||
const record = await this.orderModel.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 };
|
||
|
||
const vo: any = record.toJSON();
|
||
await this.decorate([vo]);
|
||
const details = await this.detailModel.findAll({ where: { maintenanceId: id }, order: [["id", "ASC"]] });
|
||
vo.details = details.map((d) => d.toJSON());
|
||
return { code: 200, message: "获取维保派单详情成功", data: vo };
|
||
}
|
||
|
||
async createOrder(data: any, ctx: Context): Promise<any> {
|
||
const userId = ctx.state.user.userId;
|
||
if (!(await this.isCustomerOwner(userId, Number(data.customerId)))) return { code: 403, message: "无权限为该客户创建维保派单", data: null };
|
||
|
||
const exists = await this.orderModel.findOne({ where: { maintenanceNo: data.maintenanceNo } });
|
||
if (exists) return { code: 400, message: "维保单号已存在", data: null };
|
||
|
||
if (data.workerId) {
|
||
const worker = await this.workerModel.findByPk(Number(data.workerId));
|
||
if (!worker) return { code: 400, message: "共享电工不存在", data: null };
|
||
}
|
||
|
||
const payload = this.normalizePayload(data);
|
||
const details = Array.isArray(payload.details) ? payload.details : [];
|
||
delete payload.details;
|
||
if (payload.status === undefined || payload.status === null) payload.status = STATUS.DISPATCHED;
|
||
|
||
const t = await this.orderModel.sequelize!.transaction();
|
||
let created!: MaintenanceOrder;
|
||
const detailRows: MaintenanceDetail[] = [];
|
||
try {
|
||
created = await this.orderModel.create(payload, { transaction: t });
|
||
for (const item of details) {
|
||
const detailPayload = this.normalizePayload({ ...item, maintenanceId: created.id, customerId: created.customerId });
|
||
detailRows.push(await this.detailModel.create(detailPayload, { transaction: t }));
|
||
}
|
||
await t.commit();
|
||
} catch (err: any) {
|
||
await t.rollback();
|
||
if (err instanceof UniqueConstraintError) return { code: 400, message: "维保单号已存在", data: null };
|
||
throw err;
|
||
}
|
||
|
||
const vo: any = created.toJSON();
|
||
await this.decorate([vo]);
|
||
vo.details = detailRows.map((d) => d.toJSON());
|
||
await this.logService.logAction(ctx, "维保派单", "创建", created.maintenanceNo, `创建维保派单 ${created.maintenanceNo}`);
|
||
// 派单站内通知(埋点式;通知失败不得阻断业务)
|
||
if (created.workerId) {
|
||
try {
|
||
await this.noticeRuleService.notifyDispatchAssign({
|
||
orderTypeName: "维保",
|
||
orderNo: created.maintenanceNo,
|
||
customerId: Number(created.customerId),
|
||
workerId: Number(created.workerId),
|
||
detailText: created.maintenanceDate ? `维保日期:${created.maintenanceDate}。` : "",
|
||
bizType: "maintenance_order",
|
||
bizId: Number(created.id)
|
||
});
|
||
} catch (err) {
|
||
console.error("[notice] 维保派单通知失败:", err);
|
||
}
|
||
}
|
||
return { code: 200, message: "创建维保派单成功", data: vo };
|
||
}
|
||
|
||
async updateOrder(id: number, data: any, ctx: Context): Promise<any> {
|
||
const record = await this.orderModel.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.COMPLETED) return { code: 400, message: "已完成的派单不可修改", data: null };
|
||
|
||
if (data.maintenanceNo !== undefined && data.maintenanceNo !== record.maintenanceNo) {
|
||
const exists = await this.orderModel.findOne({ where: { maintenanceNo: data.maintenanceNo, id: { [Op.ne]: id } } });
|
||
if (exists) return { code: 400, message: "维保单号已存在", data: null };
|
||
}
|
||
if (data.customerId !== undefined && Number(data.customerId) !== Number(record.customerId)) {
|
||
if (!(await this.isCustomerOwner(ctx.state.user.userId, Number(data.customerId)))) {
|
||
return { code: 403, message: "无权限转移到该客户", data: null };
|
||
}
|
||
}
|
||
if (data.workerId) {
|
||
const worker = await this.workerModel.findByPk(Number(data.workerId));
|
||
if (!worker) return { code: 400, message: "共享电工不存在", data: null };
|
||
}
|
||
|
||
const payload = this.normalizePayload(data);
|
||
const details = Array.isArray(payload.details) ? payload.details : [];
|
||
delete payload.details;
|
||
|
||
const t = await this.orderModel.sequelize!.transaction();
|
||
const detailRows: MaintenanceDetail[] = [];
|
||
const oldWorkerId = record.workerId ? Number(record.workerId) : null;
|
||
try {
|
||
await record.update(payload, { transaction: t });
|
||
if (details.length) {
|
||
await this.detailModel.destroy({ where: { maintenanceId: id }, transaction: t });
|
||
for (const item of details) {
|
||
const detailPayload = this.normalizePayload({ ...item, maintenanceId: id, customerId: record.customerId });
|
||
detailRows.push(await this.detailModel.create(detailPayload, { transaction: t }));
|
||
}
|
||
}
|
||
await t.commit();
|
||
} catch (err: any) {
|
||
await t.rollback();
|
||
throw err;
|
||
}
|
||
|
||
const vo: any = record.toJSON();
|
||
await this.decorate([vo]);
|
||
vo.details = detailRows.map((d) => d.toJSON());
|
||
await this.logService.logAction(ctx, "维保派单", "更新", record.maintenanceNo, `更新维保派单 ${record.maintenanceNo}`);
|
||
// 改派站内通知(派单对象变化时提醒新电工)
|
||
if (record.workerId && Number(record.workerId) !== oldWorkerId) {
|
||
try {
|
||
await this.noticeRuleService.notifyDispatchAssign({
|
||
orderTypeName: "维保",
|
||
orderNo: record.maintenanceNo,
|
||
customerId: Number(record.customerId),
|
||
workerId: Number(record.workerId),
|
||
detailText: record.maintenanceDate ? `维保日期:${record.maintenanceDate}。` : "",
|
||
bizType: "maintenance_order",
|
||
bizId: Number(record.id)
|
||
});
|
||
} catch (err) {
|
||
console.error("[notice] 维保派单通知失败:", err);
|
||
}
|
||
}
|
||
return { code: 200, message: "更新维保派单成功", data: vo };
|
||
}
|
||
|
||
// 完成派单:仅允许 0已派发 → 1已完成
|
||
async completeOrder(id: number, ctx: Context): Promise<any> {
|
||
const record = await this.orderModel.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.DISPATCHED) return { code: 400, message: "仅已派发状态的派单可标记完成", data: null };
|
||
|
||
await record.update({ status: STATUS.COMPLETED });
|
||
const vo: any = record.toJSON();
|
||
await this.decorate([vo]);
|
||
await this.logService.logAction(ctx, "维保派单", "完成", record.maintenanceNo, `完成维保派单 ${record.maintenanceNo}`);
|
||
return { code: 200, message: "维保派单已完成", data: vo };
|
||
}
|
||
|
||
async removeOrder(id: number, ctx: Context): Promise<any> {
|
||
const record = await this.orderModel.findByPk(id);
|
||
if (!record) return { code: 404, message: "维保派单不存在", data: null };
|
||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该维保派单", data: null };
|
||
|
||
const maintenanceNo = record.maintenanceNo;
|
||
const t = await this.orderModel.sequelize!.transaction();
|
||
try {
|
||
await this.detailModel.destroy({ where: { maintenanceId: id }, transaction: t });
|
||
await record.destroy({ transaction: t });
|
||
await t.commit();
|
||
} catch (err: any) {
|
||
await t.rollback();
|
||
throw err;
|
||
}
|
||
await this.logService.logAction(ctx, "维保派单", "删除", maintenanceNo, `删除维保派单 ${maintenanceNo}`);
|
||
return { code: 200, message: "删除维保派单成功", data: null };
|
||
}
|
||
}
|