280 lines
12 KiB
TypeScript
280 lines
12 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 InstallOrder from "../entity/install_order.entity";
|
|
import Customer from "../entity/customer.entity";
|
|
import Worker from "../entity/worker.entity";
|
|
import ProjectPlan from "../entity/project_plan.entity";
|
|
import SysUserRole from "../entity/sysUserRole.entity";
|
|
import SysRole from "../entity/sysRole.entity";
|
|
import { LogService } from "./log.service";
|
|
|
|
// 安装派单状态:0已派发 1已完成
|
|
const STATUS = { DISPATCHED: 0, COMPLETED: 1 };
|
|
|
|
@Provide()
|
|
export class InstallOrderService {
|
|
@InjectRepository(InstallOrder)
|
|
installModel!: Repository<InstallOrder>;
|
|
|
|
@InjectRepository(Customer)
|
|
customerModel!: Repository<Customer>;
|
|
|
|
@InjectRepository(Worker)
|
|
workerModel!: Repository<Worker>;
|
|
|
|
@InjectRepository(ProjectPlan)
|
|
planModel!: Repository<ProjectPlan>;
|
|
|
|
@InjectRepository(SysUserRole)
|
|
userRoleModel!: Repository<SysUserRole>;
|
|
|
|
@InjectRepository(SysRole)
|
|
roleModel!: Repository<SysRole>;
|
|
|
|
@Inject()
|
|
logService!: LogService;
|
|
|
|
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: InstallOrder): Promise<boolean> {
|
|
if (await this.isAdmin(userId)) return true;
|
|
return this.isCustomerOwner(userId, order.customerId);
|
|
}
|
|
|
|
// 单号规则:WH-INST-YYYY-NNNN(年内累计序号),冲突重试 1 次
|
|
private async generateInstallNo(retry: number = 0): Promise<string> {
|
|
const now = new Date();
|
|
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
|
const year = d.getUTCFullYear();
|
|
const prefix = `WH-INST-${year}-`;
|
|
const last = await this.installModel.findOne({ where: { installNo: { [Op.like]: `${prefix}%` } }, order: [["install_no", "DESC"]] });
|
|
let seq = 1;
|
|
if (last) seq = (parseInt(last.installNo.slice(-4), 10) || 0) + 1;
|
|
const result = `${prefix}${String(seq).padStart(4, "0")}`;
|
|
const exists = await this.installModel.findOne({ where: { installNo: result } });
|
|
if (exists) {
|
|
if (retry < 1) return this.generateInstallNo(retry + 1);
|
|
throw new Error("安装单号生成冲突,请重试");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// query 参数 → Sequelize where(关键字/客户/状态/派单日期范围)
|
|
private buildWhere(query: any): any {
|
|
const where: any = {};
|
|
if (query.keyword) {
|
|
where[Op.or] = [
|
|
{ installNo: { [Op.like]: `%${query.keyword}%` } },
|
|
{ installer: { [Op.like]: `%${query.keyword}%` } },
|
|
{ remark: { [Op.like]: `%${query.keyword}%` } }
|
|
];
|
|
}
|
|
if (query.customerId !== undefined && query.customerId !== "") where.customerId = Number(query.customerId);
|
|
if (query.status !== undefined && query.status !== "") where.status = Number(query.status);
|
|
// dateRange: [startDate, endDate],作用于 dispatch_date
|
|
const range = Array.isArray(query.dateRange) ? query.dateRange : (typeof query.dateRange === "string" ? query.dateRange.split(",") : null);
|
|
if (range && range.length === 2 && (range[0] || range[1])) {
|
|
const cond: any = {};
|
|
if (range[0]) cond[Op.gte] = range[0];
|
|
if (range[1]) cond[Op.lte] = range[1];
|
|
where.dispatchDate = 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 planIds = [...new Set(rows.map((r) => r.planId).filter(Boolean))];
|
|
|
|
const [customers, workers, plans] = 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([]),
|
|
planIds.length
|
|
? this.planModel.findAll({
|
|
where: { id: { [Op.in]: planIds } },
|
|
attributes: ["id", "planNo", "planDesc", "status"]
|
|
})
|
|
: Promise.resolve([])
|
|
]);
|
|
|
|
const customerMap = new Map(customers.map((c) => [c.id, c]));
|
|
const workerMap = new Map(workers.map((w) => [w.id, w]));
|
|
const planMap = new Map(plans.map((p) => [p.id, p]));
|
|
|
|
for (const row of rows) {
|
|
const customer: any = customerMap.get(row.customerId);
|
|
const worker: any = workerMap.get(row.workerId);
|
|
const plan: any = planMap.get(row.planId);
|
|
row.customer = customer ? customer.toJSON() : null;
|
|
row.customerName = customer?.customerName || "";
|
|
row.worker = worker ? worker.toJSON() : null;
|
|
row.workerName = worker?.workerName || "";
|
|
row.plan = plan ? plan.toJSON() : null;
|
|
row.planNo = plan?.planNo || "";
|
|
}
|
|
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 list(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.installModel.findAndCountAll({
|
|
where,
|
|
limit: pageSize,
|
|
offset: (page - 1) * pageSize,
|
|
order: [["created_at", "DESC"]],
|
|
distinct: true
|
|
});
|
|
|
|
const list = await this.decorate(rows.map((r) => r.toJSON()));
|
|
return { code: 200, message: "获取安装派单列表成功", data: { list, total: count, page, pageSize } };
|
|
}
|
|
|
|
async detail(id: number, ctx: Context): Promise<any> {
|
|
const record = await this.installModel.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]);
|
|
return { code: 200, message: "获取安装派单详情成功", data: vo };
|
|
}
|
|
|
|
async create(data: any, ctx: Context): Promise<any> {
|
|
const userId = ctx.state.user.userId;
|
|
if (!(await this.isCustomerOwner(userId, data.customerId))) return { code: 403, message: "无权限为该客户创建安装派单", data: null };
|
|
|
|
// 方案与客户一致性校验,防止串客户派单
|
|
const plan = await this.planModel.findByPk(Number(data.planId));
|
|
if (!plan) return { code: 400, message: "关联方案不存在", data: null };
|
|
if (Number(plan.customerId) !== Number(data.customerId)) 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);
|
|
payload.installNo = await this.generateInstallNo();
|
|
if (payload.status === undefined || payload.status === null) payload.status = STATUS.DISPATCHED;
|
|
|
|
let created: InstallOrder;
|
|
try {
|
|
created = await this.installModel.create(payload);
|
|
} catch (err: any) {
|
|
if (err instanceof UniqueConstraintError) {
|
|
payload.installNo = await this.generateInstallNo();
|
|
created = await this.installModel.create(payload);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
const vo: any = created.toJSON();
|
|
await this.decorate([vo]);
|
|
await this.logService.logAction(ctx, "安装派单", "创建", created.installNo, `创建安装派单 ${created.installNo}`);
|
|
return { code: 200, message: "创建安装派单成功", data: vo };
|
|
}
|
|
|
|
async update(id: number, data: any, ctx: Context): Promise<any> {
|
|
const record = await this.installModel.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 };
|
|
|
|
delete data.installNo;
|
|
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.planId !== undefined) {
|
|
const plan = await this.planModel.findByPk(Number(data.planId));
|
|
if (!plan) return { code: 400, message: "关联方案不存在", data: null };
|
|
const targetCustomerId = data.customerId !== undefined ? Number(data.customerId) : Number(record.customerId);
|
|
if (Number(plan.customerId) !== targetCustomerId) return { code: 400, message: "方案与客户不匹配", data: null };
|
|
}
|
|
|
|
const payload = this.normalizePayload(data);
|
|
await record.update(payload);
|
|
const fresh = await this.installModel.findByPk(id);
|
|
const vo: any = fresh!.toJSON();
|
|
await this.decorate([vo]);
|
|
await this.logService.logAction(ctx, "安装派单", "更新", record.installNo, `更新安装派单 ${record.installNo}`);
|
|
return { code: 200, message: "更新安装派单成功", data: vo };
|
|
}
|
|
|
|
// 完成派单:仅允许 0已派发 → 1已完成
|
|
async complete(id: number, ctx: Context): Promise<any> {
|
|
const record = await this.installModel.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.installNo, `完成安装派单 ${record.installNo}`);
|
|
return { code: 200, message: "安装派单已完成", data: vo };
|
|
}
|
|
|
|
async delete(id: number, ctx: Context): Promise<any> {
|
|
const record = await this.installModel.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 installNo = record.installNo;
|
|
await record.destroy();
|
|
await this.logService.logAction(ctx, "安装派单", "删除", installNo, `删除安装派单 ${installNo}`);
|
|
return { code: 200, message: "删除安装派单成功", data: null };
|
|
}
|
|
}
|