更新项目管理模块中合同管理、项目安装、项目验收、回访记录等功能
This commit is contained in:
1 parent
307e9cf1b9
commit
b3cef22a54
32 files changed
+6094
-2600
No files matched your search
@@ -0,0 +1,16 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { InstallOrderService } from "../service/install_order.service";
|
||||
|
||||
@Controller("/api/install-orders")
|
||||
export class InstallOrderController {
|
||||
@Inject() ctx!: Context;
|
||||
@Inject() installOrderService!: InstallOrderService;
|
||||
|
||||
@Get("/") async list(@Query() query: any) { return this.installOrderService.list(query, this.ctx); }
|
||||
@Get("/:id") async detail(@Param("id") id: string) { return this.installOrderService.detail(Number(id), this.ctx); }
|
||||
@Post("/") async create(@Body() body: any) { return this.installOrderService.create(body, this.ctx); }
|
||||
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.installOrderService.update(Number(id), body, this.ctx); }
|
||||
@Put("/:id/complete") async complete(@Param("id") id: string) { return this.installOrderService.complete(Number(id), this.ctx); }
|
||||
@Del("/:id") async delete(@Param("id") id: string) { return this.installOrderService.delete(Number(id), this.ctx); }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsInt,
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsIn,
|
||||
ArrayNotEmpty,
|
||||
Matches,
|
||||
Min,
|
||||
Max,
|
||||
MaxLength,
|
||||
ArrayMaxSize,
|
||||
ArrayMinSize
|
||||
} from "class-validator";
|
||||
|
||||
export class InstallOrderQueryDTO {
|
||||
@IsOptional() @IsInt() @Min(1) page?: number = 1;
|
||||
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
|
||||
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsInt() @IsIn([0, 1]) status?: number;
|
||||
// 派单日期范围 [startDate, endDate],YYYY-MM-DD
|
||||
@IsOptional() @IsArray() @ArrayMinSize(2) @ArrayMaxSize(2) @IsDateString({}, { each: true }) dateRange?: string[];
|
||||
}
|
||||
|
||||
export class InstallOrderCreateDTO {
|
||||
@IsNotEmpty({ message: "所属客户不能为空" }) @IsInt() customerId!: number;
|
||||
@IsNotEmpty({ message: "关联方案不能为空" }) @IsInt() planId!: number;
|
||||
@IsNotEmpty({ message: "安装人不能为空" }) @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) installer!: string;
|
||||
@IsNotEmpty({ message: "派单日期不能为空" }) @IsDateString() dispatchDate!: string;
|
||||
|
||||
@IsOptional() @IsInt() workerId?: number;
|
||||
@IsOptional() @IsDateString() startDate?: string;
|
||||
@IsOptional() @IsDateString() endDate?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
|
||||
@IsOptional() @IsInt() @IsIn([0, 1]) status?: number;
|
||||
}
|
||||
|
||||
export class InstallOrderUpdateDTO {
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsInt() planId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) installer?: string;
|
||||
@IsOptional() @IsDateString() dispatchDate?: string;
|
||||
@IsOptional() @IsInt() workerId?: number;
|
||||
@IsOptional() @IsDateString() startDate?: string;
|
||||
@IsOptional() @IsDateString() endDate?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
|
||||
@IsOptional() @IsInt() @IsIn([0, 1]) status?: number;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import Customer from "./customer.entity";
|
||||
import Worker from "./worker.entity";
|
||||
import ProjectPlan from "./project_plan.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "install_order",
|
||||
comment: "安装派单信息表",
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{ name: "idx_install_customer", fields: ["customer_id"] },
|
||||
{ name: "idx_install_worker", fields: ["worker_id"] },
|
||||
{ name: "idx_install_plan", fields: ["plan_id"] },
|
||||
{ name: "idx_install_status", fields: ["status"] },
|
||||
{ name: "idx_install_dispatch_date", fields: ["dispatch_date"] }
|
||||
]
|
||||
})
|
||||
export default class InstallOrder extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "install_no", comment: "安装单号(唯一)" })
|
||||
installNo!: string;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), comment: "安装人" })
|
||||
installer!: string;
|
||||
|
||||
@ForeignKey(() => Worker)
|
||||
@Column({ type: DataType.BIGINT, field: "worker_id", comment: "共享电工ID" })
|
||||
workerId!: number;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "dispatch_date", comment: "派单日期" })
|
||||
dispatchDate!: string;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "start_date", comment: "开始日期" })
|
||||
startDate!: string;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "end_date", comment: "结束日期" })
|
||||
endDate!: string;
|
||||
|
||||
@ForeignKey(() => ProjectPlan)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "plan_id", comment: "方案ID" })
|
||||
planId!: number;
|
||||
|
||||
@Column({ type: DataType.TEXT, comment: "备注" })
|
||||
remark!: string;
|
||||
|
||||
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0已派发 1已完成" })
|
||||
status!: number;
|
||||
|
||||
@BelongsTo(() => Customer, { foreignKey: "customerId", as: "customer" })
|
||||
customer!: Customer;
|
||||
|
||||
@BelongsTo(() => Worker, { foreignKey: "workerId", as: "worker" })
|
||||
worker!: Worker;
|
||||
|
||||
@BelongsTo(() => ProjectPlan, { foreignKey: "planId", as: "plan" })
|
||||
plan!: ProjectPlan;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -23,6 +23,7 @@ const PERMISSION_MAP: Record<string, string> = {
|
||||
'/api/survey-orders': 'project:survey',
|
||||
'/api/project-plans': 'project:plan',
|
||||
'/api/contracts': 'project:contract',
|
||||
'/api/install-orders': 'project:install',
|
||||
'/api/acceptances': 'project:acceptance',
|
||||
'/api/visit-records': 'project:visit',
|
||||
};
|
||||
|
||||
@@ -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 } from "sequelize";
|
||||
@@ -7,6 +7,7 @@ import Acceptance from "../entity/acceptance.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
@Provide()
|
||||
export class AcceptanceService {
|
||||
@@ -22,6 +23,9 @@ export class AcceptanceService {
|
||||
@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");
|
||||
@@ -57,9 +61,18 @@ export class AcceptanceService {
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ acceptor: { [Op.like]: `%${query.keyword}%` } }, { acceptOpinion: { [Op.like]: `%${query.keyword}%` } }];
|
||||
where[Op.or] = [
|
||||
{ acceptNo: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ acceptor: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ acceptOpinion: { [Op.like]: `%${query.keyword}%` } }
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.acceptStartDate || query.acceptEndDate) {
|
||||
where.acceptDate = {};
|
||||
if (query.acceptStartDate) where.acceptDate[Op.gte] = query.acceptStartDate;
|
||||
if (query.acceptEndDate) where.acceptDate[Op.lte] = query.acceptEndDate;
|
||||
}
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
@@ -88,6 +101,7 @@ export class AcceptanceService {
|
||||
try {
|
||||
data.acceptNo = await this.generateAcceptNo();
|
||||
const created = await this.acceptModel.create(data);
|
||||
await this.logService.logAction(ctx, "项目验收", "创建", data.acceptNo, `创建项目验收 ${data.acceptNo}`);
|
||||
return { code: 200, message: "创建项目验收成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
@@ -108,6 +122,7 @@ export class AcceptanceService {
|
||||
}
|
||||
delete data.acceptNo;
|
||||
await record.update(data);
|
||||
await this.logService.logAction(ctx, "项目验收", "更新", record.acceptNo, `更新项目验收 ${record.acceptNo}`);
|
||||
return { code: 200, message: "更新项目验收成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
@@ -115,7 +130,9 @@ export class AcceptanceService {
|
||||
const record = await this.acceptModel.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 acceptNo = record.acceptNo;
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "项目验收", "删除", acceptNo, `删除项目验收 ${acceptNo}`);
|
||||
return { code: 200, message: "删除项目验收成功", data: null };
|
||||
}
|
||||
}
|
||||
@@ -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 } from "sequelize";
|
||||
@@ -7,6 +7,7 @@ import Contract from "../entity/contract.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
@Provide()
|
||||
export class ContractService {
|
||||
@@ -22,6 +23,9 @@ export class ContractService {
|
||||
@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");
|
||||
@@ -72,11 +76,20 @@ export class ContractService {
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ xtContractNo: { [Op.like]: `%${query.keyword}%` } }, { contractDesc: { [Op.like]: `%${query.keyword}%` } }];
|
||||
where[Op.or] = [
|
||||
{ contractNo: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ xtContractNo: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ contractDesc: { [Op.like]: `%${query.keyword}%` } }
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.salesmanId) where.salesmanId = query.salesmanId;
|
||||
if (query.status !== undefined) where.status = query.status;
|
||||
if (query.signStartDate || query.signEndDate) {
|
||||
where.signDate = {};
|
||||
if (query.signStartDate) where.signDate[Op.gte] = query.signStartDate;
|
||||
if (query.signEndDate) where.signDate[Op.lte] = query.signEndDate;
|
||||
}
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
@@ -109,6 +122,7 @@ export class ContractService {
|
||||
data.contractNo = await this.generateContractNo();
|
||||
if (!data.xtContractNo) data.xtContractNo = await this.generateXtContractNo();
|
||||
const created = await this.contractModel.create(data);
|
||||
await this.logService.logAction(ctx, "项目合同", "创建", data.contractNo, `创建项目合同 ${data.contractNo}`);
|
||||
return { code: 200, message: "创建项目合同成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
@@ -131,6 +145,7 @@ export class ContractService {
|
||||
}
|
||||
delete data.contractNo;
|
||||
await record.update(data);
|
||||
await this.logService.logAction(ctx, "项目合同", "更新", record.contractNo, `更新项目合同 ${record.contractNo}`);
|
||||
return { code: 200, message: "更新项目合同成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
@@ -138,7 +153,9 @@ export class ContractService {
|
||||
const record = await this.contractModel.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 contractNo = record.contractNo;
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "项目合同", "删除", contractNo, `删除项目合同 ${contractNo}`);
|
||||
return { code: 200, message: "删除项目合同成功", data: null };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import SysOperationLog from "../entity/sysOperationLog.entity";
|
||||
import { Op } from "sequelize";
|
||||
import dayjs from "dayjs";
|
||||
@@ -9,6 +10,24 @@ export class LogService {
|
||||
@InjectRepository(SysOperationLog)
|
||||
logModel!: typeof SysOperationLog;
|
||||
|
||||
// 写入操作日志:操作人userId/模块/动作/目标单号/摘要
|
||||
// method 与 ip 从 ctx 提取,targetNo+摘要 落 params(JSON) 字段
|
||||
async logAction(ctx: Context, module: string, action: string, targetNo: string, summary: string): Promise<void> {
|
||||
try {
|
||||
const userId = (ctx.state as any)?.user?.userId ?? 0;
|
||||
await this.logModel.create({
|
||||
userId,
|
||||
module,
|
||||
action,
|
||||
method: ctx.method || "",
|
||||
params: JSON.stringify({ targetNo: targetNo || "", summary: summary || "" }),
|
||||
ip: ctx.ip || ""
|
||||
} as any);
|
||||
} catch {
|
||||
// 日志写入失败不影响主业务流程
|
||||
}
|
||||
}
|
||||
|
||||
async list(query: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
||||
@@ -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 } from "sequelize";
|
||||
@@ -8,6 +8,7 @@ import ProjectPlanDetail from "../entity/project_plan_detail.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
@Provide()
|
||||
export class ProjectPlanService {
|
||||
@@ -26,6 +27,9 @@ export class ProjectPlanService {
|
||||
@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");
|
||||
@@ -61,10 +65,19 @@ export class ProjectPlanService {
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ registrar: { [Op.like]: `%${query.keyword}%` } }, { planDesc: { [Op.like]: `%${query.keyword}%` } }];
|
||||
where[Op.or] = [
|
||||
{ planNo: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ registrar: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ planDesc: { [Op.like]: `%${query.keyword}%` } }
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.status !== undefined) where.status = query.status;
|
||||
if (query.registerStartDate || query.registerEndDate) {
|
||||
where.registerTime = {};
|
||||
if (query.registerStartDate) where.registerTime[Op.gte] = query.registerStartDate;
|
||||
if (query.registerEndDate) where.registerTime[Op.lte] = query.registerEndDate;
|
||||
}
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
@@ -99,6 +112,7 @@ export class ProjectPlanService {
|
||||
await this.detailModel.bulkCreate(details.map((d: any) => ({ ...d, planId: created.id, customerId: created.customerId })));
|
||||
}
|
||||
const result = await this.planModel.findByPk(created.id, { include: [this.detailModel] });
|
||||
await this.logService.logAction(ctx, "项目方案", "创建", data.planNo, `创建项目方案 ${data.planNo}`);
|
||||
return { code: 200, message: "创建项目方案成功", data: result?.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
@@ -133,6 +147,7 @@ export class ProjectPlanService {
|
||||
}
|
||||
}
|
||||
const result = await this.planModel.findByPk(id, { include: [this.detailModel] });
|
||||
await this.logService.logAction(ctx, "项目方案", "更新", record.planNo, `更新项目方案 ${record.planNo}`);
|
||||
return { code: 200, message: "更新项目方案成功", data: result?.toJSON() };
|
||||
}
|
||||
|
||||
@@ -140,8 +155,10 @@ export class ProjectPlanService {
|
||||
const record = await this.planModel.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 planNo = record.planNo;
|
||||
await this.detailModel.destroy({ where: { planId: id } });
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "项目方案", "删除", planNo, `删除项目方案 ${planNo}`);
|
||||
return { code: 200, message: "删除项目方案成功", data: null };
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import SysUser from "../entity/sysUser.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { UploadService } from "./upload.service";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
// 勘察工单状态:0待指派 1现场勘察中 2待技术复核 3已归档 4预留 5已取消
|
||||
const STATUS = { PENDING_ASSIGN: 0, SURVEYING: 1, PENDING_REVIEW: 2, ARCHIVED: 3, RESERVED: 4, CANCELED: 5 };
|
||||
@@ -57,6 +58,9 @@ export class SurveyOrderService {
|
||||
@Inject()
|
||||
uploadService!: UploadService;
|
||||
|
||||
@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");
|
||||
@@ -479,6 +483,7 @@ export class SurveyOrderService {
|
||||
await this.createSopResults(created.id, templateCode);
|
||||
const vo = this.toVO(created);
|
||||
await this.decorate([vo]);
|
||||
await this.logService.logAction(ctx, "勘察派单", "创建", created.surveyNo, `创建勘察派单 ${created.surveyNo}`);
|
||||
return { code: 200, message: "创建勘察派单成功", data: vo };
|
||||
}
|
||||
|
||||
@@ -501,6 +506,7 @@ export class SurveyOrderService {
|
||||
const fresh = await this.surveyModel.findByPk(id);
|
||||
const vo = this.toVO(fresh!);
|
||||
await this.decorate([vo]);
|
||||
await this.logService.logAction(ctx, "勘察派单", "更新", record.surveyNo, `更新勘察派单 ${record.surveyNo}`);
|
||||
return { code: 200, message: "更新勘察派单成功", data: vo };
|
||||
}
|
||||
|
||||
@@ -535,6 +541,7 @@ export class SurveyOrderService {
|
||||
|
||||
const result = await this.assignCore(id, worker, dto.estimatedFee);
|
||||
if (!result.ok) return { code: result.code, message: result.message, data: null };
|
||||
await this.logService.logAction(ctx, "勘察派单", "指派", record.surveyNo, `指派勘察派单 ${record.surveyNo} 至 ${worker.workerName}`);
|
||||
return { code: 200, message: "派单成功", data: result.data };
|
||||
}
|
||||
|
||||
@@ -555,6 +562,7 @@ export class SurveyOrderService {
|
||||
failIds.push(id);
|
||||
}
|
||||
}
|
||||
await this.logService.logAction(ctx, "勘察派单", "批量指派", "", `批量指派 ${successCount} 条勘察派单至 ${worker.workerName},失败 ${failIds.length} 条`);
|
||||
return {
|
||||
code: 200,
|
||||
message: `批量派单完成,成功 ${successCount} 条`,
|
||||
@@ -580,6 +588,7 @@ export class SurveyOrderService {
|
||||
await record.update(patch);
|
||||
const vo = this.toVO(record);
|
||||
await this.decorate([vo]);
|
||||
await this.logService.logAction(ctx, "勘察派单", "复核", record.surveyNo, `复核勘察派单 ${record.surveyNo}: ${dto.result === "pass" ? "通过归档" : "退回勘察"}`);
|
||||
return { code: 200, message: dto.result === "pass" ? "复核通过,工单已归档" : "已退回现场勘察中", data: vo };
|
||||
}
|
||||
|
||||
@@ -594,6 +603,7 @@ export class SurveyOrderService {
|
||||
const urgeCount = (record.urgeCount || 0) + 1;
|
||||
const lastUrgedAt = new Date();
|
||||
await record.update({ urgeCount, lastUrgedAt });
|
||||
await this.logService.logAction(ctx, "勘察派单", "催办", record.surveyNo, `催办勘察派单 ${record.surveyNo}(第 ${urgeCount} 次)`);
|
||||
return { code: 200, message: "催办成功", data: { urgeCount, lastUrgedAt } };
|
||||
}
|
||||
|
||||
@@ -606,6 +616,7 @@ export class SurveyOrderService {
|
||||
}
|
||||
|
||||
await record.update({ status: STATUS.CANCELED, cancelReason: dto.reason || null });
|
||||
await this.logService.logAction(ctx, "勘察派单", "取消", record.surveyNo, `取消勘察派单 ${record.surveyNo}`);
|
||||
return { code: 200, message: "取消工单成功", data: this.toVO(record) };
|
||||
}
|
||||
|
||||
@@ -613,9 +624,11 @@ export class SurveyOrderService {
|
||||
const record = await this.surveyModel.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 surveyNo = record.surveyNo;
|
||||
// 级联清理回执后删除工单
|
||||
await this.sopResultModel.destroy({ where: { surveyOrderId: id } });
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "勘察派单", "删除", surveyNo, `删除勘察派单 ${surveyNo}`);
|
||||
return { code: 200, message: "删除勘察派单成功", data: null };
|
||||
}
|
||||
}
|
||||
@@ -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 } from "sequelize";
|
||||
@@ -7,6 +7,7 @@ import VisitRecord from "../entity/visit_record.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
@Provide()
|
||||
export class VisitRecordService {
|
||||
@@ -22,6 +23,9 @@ export class VisitRecordService {
|
||||
@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");
|
||||
@@ -65,6 +69,11 @@ export class VisitRecordService {
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.visitStartDate || query.visitEndDate) {
|
||||
where.visitTime = {};
|
||||
if (query.visitStartDate) where.visitTime[Op.gte] = query.visitStartDate;
|
||||
if (query.visitEndDate) where.visitTime[Op.lte] = query.visitEndDate;
|
||||
}
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
@@ -93,6 +102,7 @@ export class VisitRecordService {
|
||||
try {
|
||||
data.visitNo = await this.generateVisitNo();
|
||||
const created = await this.visitModel.create(data);
|
||||
await this.logService.logAction(ctx, "客户回访", "创建", data.visitNo, `创建客户回访 ${data.visitNo}`);
|
||||
return { code: 200, message: "创建客户回访成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
@@ -113,6 +123,7 @@ export class VisitRecordService {
|
||||
}
|
||||
delete data.visitNo;
|
||||
await record.update(data);
|
||||
await this.logService.logAction(ctx, "客户回访", "更新", record.visitNo, `更新客户回访 ${record.visitNo}`);
|
||||
return { code: 200, message: "更新客户回访成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
@@ -120,7 +131,9 @@ export class VisitRecordService {
|
||||
const record = await this.visitModel.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 visitNo = record.visitNo;
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "客户回访", "删除", visitNo, `删除客户回访 ${visitNo}`);
|
||||
return { code: 200, message: "删除客户回访成功", data: null };
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user