feat: 新增客户拜访、建筑、房间及项目全流程业务模块

本次提交新增了客户拜访、客户建筑、客户房间的完整业务模块,同时补充了项目勘察、项目方案、合同管理、项目验收、回访记录的全项目流程相关功能,包括:
1.  新增对应业务的实体类、DTO、控制器、服务层代码
2.  完善RBAC权限映射配置
3.  添加前端API、路由及页面菜单配置
4.  更新数据库种子数据补充菜单和权限绑定
This commit is contained in:
andyesfly committed 2026-08-26 14:08:08 +08:00
1 parent f61ade107b
commit 219091f864
53 files changed
+5990 -6

No files matched your search

@@ -0,0 +1,15 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { AcceptanceService } from "../service/acceptance.service";
@Controller("/api/acceptances")
export class AcceptanceController {
@Inject() ctx!: Context;
@Inject() acceptanceService!: AcceptanceService;
@Get("/") async list(@Query() query: any) { return this.acceptanceService.list(query, this.ctx); }
@Get("/:id") async detail(@Param("id") id: string) { return this.acceptanceService.detail(Number(id), this.ctx); }
@Post("/") async create(@Body() body: any) { return this.acceptanceService.create(body, this.ctx); }
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.acceptanceService.update(Number(id), body, this.ctx); }
@Del("/:id") async delete(@Param("id") id: string) { return this.acceptanceService.delete(Number(id), this.ctx); }
}
@@ -0,0 +1,15 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { ContractService } from "../service/contract.service";
@Controller("/api/contracts")
export class ContractController {
@Inject() ctx!: Context;
@Inject() contractService!: ContractService;
@Get("/") async list(@Query() query: any) { return this.contractService.list(query, this.ctx); }
@Get("/:id") async detail(@Param("id") id: string) { return this.contractService.detail(Number(id), this.ctx); }
@Post("/") async create(@Body() body: any) { return this.contractService.create(body, this.ctx); }
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.contractService.update(Number(id), body, this.ctx); }
@Del("/:id") async delete(@Param("id") id: string) { return this.contractService.delete(Number(id), this.ctx); }
}
@@ -0,0 +1,15 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { CustomerBuildingService } from "../service/customer_building.service";
@Controller("/api/customer-buildings")
export class CustomerBuildingController {
@Inject() ctx!: Context;
@Inject() customerBuildingService!: CustomerBuildingService;
@Get("/") async list(@Query() query: any) { return this.customerBuildingService.list(query, this.ctx); }
@Get("/:id") async detail(@Param("id") id: string) { return this.customerBuildingService.detail(Number(id), this.ctx); }
@Post("/") async create(@Body() body: any) { return this.customerBuildingService.create(body, this.ctx); }
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.customerBuildingService.update(Number(id), body, this.ctx); }
@Del("/:id") async delete(@Param("id") id: string) { return this.customerBuildingService.delete(Number(id), this.ctx); }
}
@@ -0,0 +1,15 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { CustomerRoomService } from "../service/customer_room.service";
@Controller("/api/customer-rooms")
export class CustomerRoomController {
@Inject() ctx!: Context;
@Inject() customerRoomService!: CustomerRoomService;
@Get("/") async list(@Query() query: any) { return this.customerRoomService.list(query, this.ctx); }
@Get("/:id") async detail(@Param("id") id: string) { return this.customerRoomService.detail(Number(id), this.ctx); }
@Post("/") async create(@Body() body: any) { return this.customerRoomService.create(body, this.ctx); }
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.customerRoomService.update(Number(id), body, this.ctx); }
@Del("/:id") async delete(@Param("id") id: string) { return this.customerRoomService.delete(Number(id), this.ctx); }
}
@@ -0,0 +1,15 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { CustomerVisitService } from "../service/customer_visit.service";
@Controller("/api/customer-visits")
export class CustomerVisitController {
@Inject() ctx!: Context;
@Inject() customerVisitService!: CustomerVisitService;
@Get("/") async list(@Query() query: any) { return this.customerVisitService.list(query, this.ctx); }
@Get("/:id") async detail(@Param("id") id: string) { return this.customerVisitService.detail(Number(id), this.ctx); }
@Post("/") async create(@Body() body: any) { return this.customerVisitService.create(body, this.ctx); }
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.customerVisitService.update(Number(id), body, this.ctx); }
@Del("/:id") async delete(@Param("id") id: string) { return this.customerVisitService.delete(Number(id), this.ctx); }
}
@@ -0,0 +1,15 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { ProjectPlanService } from "../service/project_plan.service";
@Controller("/api/project-plans")
export class ProjectPlanController {
@Inject() ctx!: Context;
@Inject() projectPlanService!: ProjectPlanService;
@Get("/") async list(@Query() query: any) { return this.projectPlanService.list(query, this.ctx); }
@Get("/:id") async detail(@Param("id") id: string) { return this.projectPlanService.detail(Number(id), this.ctx); }
@Post("/") async create(@Body() body: any) { return this.projectPlanService.create(body, this.ctx); }
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.projectPlanService.update(Number(id), body, this.ctx); }
@Del("/:id") async delete(@Param("id") id: string) { return this.projectPlanService.delete(Number(id), this.ctx); }
}
@@ -0,0 +1,15 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { SurveyOrderService } from "../service/survey_order.service";
@Controller("/api/survey-orders")
export class SurveyOrderController {
@Inject() ctx!: Context;
@Inject() surveyOrderService!: SurveyOrderService;
@Get("/") async list(@Query() query: any) { return this.surveyOrderService.list(query, this.ctx); }
@Get("/:id") async detail(@Param("id") id: string) { return this.surveyOrderService.detail(Number(id), 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); }
@Del("/:id") async delete(@Param("id") id: string) { return this.surveyOrderService.delete(Number(id), this.ctx); }
}
@@ -0,0 +1,15 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { VisitRecordService } from "../service/visit_record.service";
@Controller("/api/visit-records")
export class VisitRecordController {
@Inject() ctx!: Context;
@Inject() visitRecordService!: VisitRecordService;
@Get("/") async list(@Query() query: any) { return this.visitRecordService.list(query, this.ctx); }
@Get("/:id") async detail(@Param("id") id: string) { return this.visitRecordService.detail(Number(id), this.ctx); }
@Post("/") async create(@Body() body: any) { return this.visitRecordService.create(body, this.ctx); }
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.visitRecordService.update(Number(id), body, this.ctx); }
@Del("/:id") async delete(@Param("id") id: string) { return this.visitRecordService.delete(Number(id), this.ctx); }
}
+28
View File
@@ -0,0 +1,28 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, Matches, Min, Max, MaxLength } from "class-validator";
export class AcceptanceQueryDTO {
@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;
}
export class AcceptanceCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) acceptor?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) acceptOpinion?: string;
@IsOptional() @IsString() @MaxLength(10) acceptDate?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) installer?: string;
@IsOptional() @IsString() @MaxLength(512) acceptFileUrl?: string;
@IsOptional() @IsInt() contractId?: number;
}
export class AcceptanceUpdateDTO {
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) acceptor?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) acceptOpinion?: string;
@IsOptional() @IsString() @MaxLength(10) acceptDate?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) installer?: string;
@IsOptional() @IsString() @MaxLength(512) acceptFileUrl?: string;
@IsOptional() @IsInt() contractId?: number;
}
+40
View File
@@ -0,0 +1,40 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, IsNumber, Matches, Min, Max, MaxLength } from "class-validator";
export class ContractQueryDTO {
@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() salesmanId?: number;
@IsOptional() @IsInt() status?: number;
}
export class ContractCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) xtContractNo?: string;
@IsOptional() @IsNumber() @Min(0) contractAmount?: number;
@IsOptional() @IsString() @MaxLength(32) @Matches(/^[^<>]*$/) payCycle?: string;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsString() @MaxLength(10) signDate?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) contractDesc?: string;
@IsOptional() @IsString() @MaxLength(10) startDate?: string;
@IsOptional() @IsString() @MaxLength(10) endDate?: string;
@IsOptional() @IsInt() planId?: number;
@IsOptional() @IsString() @MaxLength(512) contractFileUrl?: string;
@IsOptional() @IsInt() @Min(0) @Max(1) status?: number;
}
export class ContractUpdateDTO {
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) xtContractNo?: string;
@IsOptional() @IsNumber() @Min(0) contractAmount?: number;
@IsOptional() @IsString() @MaxLength(32) @Matches(/^[^<>]*$/) payCycle?: string;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsString() @MaxLength(10) signDate?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) contractDesc?: string;
@IsOptional() @IsString() @MaxLength(10) startDate?: string;
@IsOptional() @IsString() @MaxLength(10) endDate?: string;
@IsOptional() @IsInt() planId?: number;
@IsOptional() @IsString() @MaxLength(512) contractFileUrl?: string;
@IsOptional() @IsInt() @Min(0) @Max(1) status?: number;
}
+24
View File
@@ -0,0 +1,24 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, Matches, Min, Max, MaxLength } from "class-validator";
export class CustomerBuildingQueryDTO {
@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;
}
export class CustomerBuildingCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
@IsNotEmpty({ message: '建筑名称不能为空' }) @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) buildingName!: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) basicInfo?: string;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) purpose?: string;
@IsOptional() @IsInt() @Min(0) floorCount?: number;
}
export class CustomerBuildingUpdateDTO {
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) buildingName?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) basicInfo?: string;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) purpose?: string;
@IsOptional() @IsInt() @Min(0) floorCount?: number;
}
+29
View File
@@ -0,0 +1,29 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, Matches, Min, Max, MaxLength } from "class-validator";
export class CustomerRoomQueryDTO {
@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() buildingId?: number;
}
export class CustomerRoomCreateDTO {
@IsNotEmpty({ message: '房间编码不能为空' }) @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) roomCode!: string;
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
@IsNotEmpty({ message: '房间名称不能为空' }) @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) roomName!: string;
@IsNotEmpty({ message: '所属建筑不能为空' }) @IsInt() buildingId!: number;
@IsOptional() @IsString() @MaxLength(16) @Matches(/^[^<>]*$/) floorNo?: string;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) roomPurpose?: string;
@IsOptional() @IsInt() deptId?: number;
}
export class CustomerRoomUpdateDTO {
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) roomCode?: string;
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) roomName?: string;
@IsOptional() @IsInt() buildingId?: number;
@IsOptional() @IsString() @MaxLength(16) @Matches(/^[^<>]*$/) floorNo?: string;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) roomPurpose?: string;
@IsOptional() @IsInt() deptId?: number;
}
+29
View File
@@ -0,0 +1,29 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, IsNumber, Matches, Min, Max, MaxLength } from "class-validator";
export class CustomerVisitQueryDTO {
@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() salesmanId?: number;
}
export class CustomerVisitCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
@IsNotEmpty({ message: '拜访人不能为空' }) @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitPerson!: string;
@IsOptional() @Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' }) visitPhone?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) visitContent?: string;
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsInt() @Min(0) @Max(1) willPlan?: number;
}
export class CustomerVisitUpdateDTO {
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitPerson?: string;
@IsOptional() @Matches(/^1[3-9]\d{9}$/) visitPhone?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) visitContent?: string;
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsInt() @Min(0) @Max(1) willPlan?: number;
}
+39
View File
@@ -0,0 +1,39 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, IsNumber, IsArray, ValidateNested, Matches, Min, Max, MaxLength } from "class-validator";
import { Type } from "class-transformer";
export class PlanDetailDTO {
@IsOptional() @IsInt() id?: number;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) equipName?: string;
@IsOptional() @IsInt() equipId?: number;
@IsOptional() @IsInt() smartDeviceId?: number;
@IsOptional() @IsNumber() @Min(0.01) quantity?: number;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
}
export class ProjectPlanQueryDTO {
@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() status?: number;
}
export class ProjectPlanCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) registrar?: string;
@IsOptional() @IsString() @MaxLength(10) registerTime?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) planDesc?: string;
@IsOptional() @IsString() @MaxLength(512) planFileUrl?: string;
@IsOptional() @IsInt() @Min(0) @Max(1) status?: number;
@IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => PlanDetailDTO) details?: PlanDetailDTO[];
}
export class ProjectPlanUpdateDTO {
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) registrar?: string;
@IsOptional() @IsString() @MaxLength(10) registerTime?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) planDesc?: string;
@IsOptional() @IsString() @MaxLength(512) planFileUrl?: string;
@IsOptional() @IsInt() @Min(0) @Max(1) status?: number;
@IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => PlanDetailDTO) details?: PlanDetailDTO[];
}
+27
View File
@@ -0,0 +1,27 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, Matches, Min, Max, MaxLength } from "class-validator";
export class SurveyOrderQueryDTO {
@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() status?: number;
}
export class SurveyOrderCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) surveyor?: string;
@IsOptional() @IsInt() workerId?: number;
@IsOptional() @IsString() surveyTime?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
@IsOptional() @IsInt() @Min(0) @Max(2) status?: number;
}
export class SurveyOrderUpdateDTO {
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) surveyor?: string;
@IsOptional() @IsInt() workerId?: number;
@IsOptional() @IsString() surveyTime?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
@IsOptional() @IsInt() @Min(0) @Max(2) status?: number;
}
+28
View File
@@ -0,0 +1,28 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, IsArray, IsObject, Matches, Min, Max, MaxLength } from "class-validator";
export class VisitRecordQueryDTO {
@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;
}
export class VisitRecordCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) contactPerson?: string;
@IsOptional() @Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' }) contactPhone?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitor?: string;
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) customerOpinion?: string;
@IsOptional() @IsArray() photos?: any[];
}
export class VisitRecordUpdateDTO {
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) contactPerson?: string;
@IsOptional() @Matches(/^1[3-9]\d{9}$/) contactPhone?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitor?: string;
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) customerOpinion?: string;
@IsOptional() @IsArray() photos?: any[];
}
+51
View File
@@ -0,0 +1,51 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import Customer from "./customer.entity";
import Contract from "./contract.entity";
@Table({
tableName: "acceptance",
comment: "项目验收信息表",
timestamps: true,
createdAt: "created_at",
updatedAt: false,
indexes: [{ name: "idx_accept_customer", fields: ["customer_id"] }]
})
export default class Acceptance extends Model {
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
id!: number;
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "accept_no", comment: "验收单号(唯一)" })
acceptNo!: string;
@ForeignKey(() => Customer)
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
customerId!: number;
@Column({ type: DataType.STRING(64), comment: "验收人" })
acceptor!: string;
@Column({ type: DataType.TEXT, field: "accept_opinion", comment: "验收意见" })
acceptOpinion!: string;
@Column({ type: DataType.DATEONLY, field: "accept_date", comment: "验收日期" })
acceptDate!: string;
@Column({ type: DataType.STRING(64), comment: "安装人" })
installer!: string;
@Column({ type: DataType.STRING(512), field: "accept_file_url", comment: "验收文件" })
acceptFileUrl!: string;
@ForeignKey(() => Contract)
@Column({ type: DataType.BIGINT, field: "contract_id", comment: "合同ID" })
contractId!: number;
@BelongsTo(() => Customer)
customer!: Customer;
@BelongsTo(() => Contract)
contract!: Contract;
@CreatedAt
created_at!: Date;
}
+69
View File
@@ -0,0 +1,69 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import Customer from "./customer.entity";
import ProjectPlan from "./project_plan.entity";
@Table({
tableName: "contract",
comment: "智电项目合同信息表",
timestamps: true,
createdAt: "created_at",
updatedAt: false,
indexes: [
{ name: "idx_contract_customer", fields: ["customer_id"] },
{ name: "idx_contract_xt", fields: ["xt_contract_no"] }
]
})
export default class Contract extends Model {
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
id!: number;
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "contract_no", comment: "合同单号(唯一)" })
contractNo!: string;
@Column({ type: DataType.STRING(64), field: "xt_contract_no", comment: "鑫潼合同号(XT+年份+序号)" })
xtContractNo!: string;
@ForeignKey(() => Customer)
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
customerId!: number;
@Column({ type: DataType.DECIMAL(14, 2), field: "contract_amount", comment: "合同金额" })
contractAmount!: number;
@Column({ type: DataType.STRING(32), field: "pay_cycle", comment: "支付周期:按月/按年" })
payCycle!: string;
@Column({ type: DataType.BIGINT, field: "salesman_id", comment: "业务员" })
salesmanId!: number;
@Column({ type: DataType.DATEONLY, field: "sign_date", comment: "签订日期" })
signDate!: string;
@Column({ type: DataType.TEXT, field: "contract_desc", comment: "合同说明" })
contractDesc!: 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, field: "plan_id", comment: "方案ID" })
planId!: number;
@Column({ type: DataType.STRING(512), field: "contract_file_url", comment: "签字盖章合同文件" })
contractFileUrl!: string;
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 1, comment: "状态:1有效 0终止" })
status!: number;
@BelongsTo(() => Customer)
customer!: Customer;
@BelongsTo(() => ProjectPlan)
plan!: ProjectPlan;
@CreatedAt
created_at!: Date;
}
@@ -0,0 +1,37 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import Customer from "./customer.entity";
@Table({
tableName: "customer_building",
comment: "客户建筑信息表",
timestamps: true,
createdAt: "created_at",
updatedAt: false,
indexes: [{ name: "idx_building_customer", fields: ["customer_id"] }]
})
export default class CustomerBuilding extends Model {
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
id!: number;
@ForeignKey(() => Customer)
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
customerId!: number;
@Column({ type: DataType.STRING(128), allowNull: false, field: "building_name", comment: "建筑名称" })
buildingName!: string;
@Column({ type: DataType.TEXT, field: "basic_info", comment: "基本情况" })
basicInfo!: string;
@Column({ type: DataType.STRING(128), comment: "用途" })
purpose!: string;
@Column({ type: DataType.INTEGER, field: "floor_count", comment: "层数" })
floorCount!: number;
@BelongsTo(() => Customer)
customer!: Customer;
@CreatedAt
created_at!: Date;
}
+53
View File
@@ -0,0 +1,53 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import Customer from "./customer.entity";
import CustomerBuilding from "./customer_building.entity";
import CustomerDepartment from "./customer_department.entity";
@Table({
tableName: "customer_room",
comment: "客户房间信息表",
timestamps: true,
createdAt: "created_at",
updatedAt: false,
indexes: [
{ name: "idx_room_customer", fields: ["customer_id"] },
{ name: "idx_room_building", fields: ["building_id"] }
]
})
export default class CustomerRoom extends Model {
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
id!: number;
@Column({ type: DataType.STRING(64), allowNull: false, field: "room_code", comment: "房间编码" })
roomCode!: string;
@ForeignKey(() => Customer)
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
customerId!: number;
@Column({ type: DataType.STRING(128), allowNull: false, field: "room_name", comment: "房间名称" })
roomName!: string;
@ForeignKey(() => CustomerBuilding)
@Column({ type: DataType.BIGINT, allowNull: false, field: "building_id", comment: "所属建筑ID" })
buildingId!: number;
@Column({ type: DataType.STRING(16), field: "floor_no", comment: "所在楼层" })
floorNo!: string;
@Column({ type: DataType.STRING(128), field: "room_purpose", comment: "房间用途" })
roomPurpose!: string;
@ForeignKey(() => CustomerDepartment)
@Column({ type: DataType.BIGINT, field: "dept_id", comment: "房间所属部门ID" })
deptId!: number;
@BelongsTo(() => Customer)
customer!: Customer;
@BelongsTo(() => CustomerBuilding)
building!: CustomerBuilding;
@CreatedAt
created_at!: Date;
}
@@ -0,0 +1,49 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import Customer from "./customer.entity";
@Table({
tableName: "customer_visit",
comment: "客户拜访信息表",
timestamps: true,
createdAt: "created_at",
updatedAt: false,
indexes: [
{ name: "idx_visit_customer", fields: ["customer_id"] },
{ name: "idx_visit_salesman", fields: ["salesman_id"] }
]
})
export default class CustomerVisit extends Model {
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
id!: number;
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "visit_no", comment: "拜访单号(唯一)" })
visitNo!: string;
@ForeignKey(() => Customer)
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
customerId!: number;
@Column({ type: DataType.STRING(64), allowNull: false, field: "visit_person", comment: "拜访人" })
visitPerson!: string;
@Column({ type: DataType.STRING(20), field: "visit_phone", comment: "拜访人电话" })
visitPhone!: string;
@Column({ type: DataType.TEXT, field: "visit_content", comment: "拜访内容" })
visitContent!: string;
@Column({ type: DataType.DATEONLY, field: "visit_time", comment: "拜访时间" })
visitTime!: string;
@Column({ type: DataType.BIGINT, field: "salesman_id", comment: "业务员(关联sys_user.id)" })
salesmanId!: number;
@Column({ type: DataType.SMALLINT, defaultValue: 0, field: "will_plan", comment: "是否做方案:0不同意 1同意" })
willPlan!: number;
@BelongsTo(() => Customer)
customer!: Customer;
@CreatedAt
created_at!: Date;
}
+47
View File
@@ -0,0 +1,47 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo, HasMany } from "sequelize-typescript";
import Customer from "./customer.entity";
import ProjectPlanDetail from "./project_plan_detail.entity";
@Table({
tableName: "project_plan",
comment: "智电改造方案信息表",
timestamps: true,
createdAt: "created_at",
updatedAt: false,
indexes: [{ name: "idx_plan_customer", fields: ["customer_id"] }]
})
export default class ProjectPlan extends Model {
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
id!: number;
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "plan_no", comment: "方案单号(唯一)" })
planNo!: string;
@ForeignKey(() => Customer)
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
customerId!: number;
@Column({ type: DataType.STRING(64), comment: "方案登记人(业务员)" })
registrar!: string;
@Column({ type: DataType.DATEONLY, field: "register_time", comment: "方案登记时间" })
registerTime!: string;
@Column({ type: DataType.TEXT, field: "plan_desc", comment: "方案说明" })
planDesc!: string;
@Column({ type: DataType.STRING(512), field: "plan_file_url", comment: "方案最终文件" })
planFileUrl!: string;
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0草稿 1已确认" })
status!: number;
@BelongsTo(() => Customer)
customer!: Customer;
@HasMany(() => ProjectPlanDetail, { foreignKey: "planId", onDelete: "CASCADE" })
details!: ProjectPlanDetail[];
@CreatedAt
created_at!: Date;
}
@@ -0,0 +1,48 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import ProjectPlan from "./project_plan.entity";
import Customer from "./customer.entity";
@Table({
tableName: "project_plan_detail",
comment: "项目方案设备明细表",
timestamps: true,
createdAt: "created_at",
updatedAt: false,
indexes: [
{ name: "idx_pd_plan", fields: ["plan_id"] },
{ name: "idx_pd_customer", fields: ["customer_id"] }
]
})
export default class ProjectPlanDetail extends Model {
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
id!: number;
@ForeignKey(() => ProjectPlan)
@Column({ type: DataType.BIGINT, allowNull: false, field: "plan_id", comment: "方案ID" })
planId!: number;
@ForeignKey(() => Customer)
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
customerId!: number;
@Column({ type: DataType.STRING(128), field: "equip_name", comment: "设备名称(设备域未实现前为自由文本)" })
equipName!: string;
@Column({ type: DataType.BIGINT, field: "equip_id", comment: "客户设备ID(预留)" })
equipId!: number;
@Column({ type: DataType.BIGINT, field: "smart_device_id", comment: "智电设备型号ID(预留)" })
smartDeviceId!: number;
@Column({ type: DataType.DECIMAL(10, 2), allowNull: false, defaultValue: 1, comment: "数量" })
quantity!: number;
@Column({ type: DataType.TEXT, comment: "备注" })
remark!: string;
@BelongsTo(() => ProjectPlan)
plan!: ProjectPlan;
@CreatedAt
created_at!: Date;
}
+46
View File
@@ -0,0 +1,46 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import Customer from "./customer.entity";
@Table({
tableName: "survey_order",
comment: "客户勘察派单信息表",
timestamps: true,
createdAt: "created_at",
updatedAt: false,
indexes: [
{ name: "idx_survey_customer", fields: ["customer_id"] },
{ name: "idx_survey_worker", fields: ["worker_id"] }
]
})
export default class SurveyOrder extends Model {
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
id!: number;
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "survey_no", comment: "勘察单号(唯一)" })
surveyNo!: string;
@ForeignKey(() => Customer)
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
customerId!: number;
@Column({ type: DataType.STRING(64), comment: "勘察人(共享电工)" })
surveyor!: string;
@Column({ type: DataType.BIGINT, field: "worker_id", comment: "共享电工ID" })
workerId!: number;
@Column({ type: DataType.DATE, field: "survey_time", comment: "勘察时间" })
surveyTime!: Date;
@Column({ type: DataType.TEXT, comment: "备注" })
remark!: string;
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0已派发 1已接单 2已完成" })
status!: number;
@BelongsTo(() => Customer)
customer!: Customer;
@CreatedAt
created_at!: Date;
}
+46
View File
@@ -0,0 +1,46 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import Customer from "./customer.entity";
@Table({
tableName: "visit_record",
comment: "客户回访信息表",
timestamps: true,
createdAt: "created_at",
updatedAt: false,
indexes: [{ name: "idx_visit_rec_customer", fields: ["customer_id"] }]
})
export default class VisitRecord extends Model {
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
id!: number;
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "visit_no", comment: "回访单号(唯一)" })
visitNo!: string;
@ForeignKey(() => Customer)
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
customerId!: number;
@Column({ type: DataType.STRING(64), field: "contact_person", comment: "联系人" })
contactPerson!: string;
@Column({ type: DataType.STRING(20), field: "contact_phone", comment: "联系电话" })
contactPhone!: string;
@Column({ type: DataType.STRING(64), field: "visitor", comment: "回访人" })
visitor!: string;
@Column({ type: DataType.DATEONLY, field: "visit_time", comment: "回访时间" })
visitTime!: string;
@Column({ type: DataType.TEXT, field: "customer_opinion", comment: "客户意见" })
customerOpinion!: string;
@Column({ type: DataType.JSONB, field: "photos", comment: "回访照片(JSONB)" })
photos!: any;
@BelongsTo(() => Customer)
customer!: Customer;
@CreatedAt
created_at!: Date;
}
+8
View File
@@ -16,6 +16,14 @@ const PERMISSION_MAP: Record<string, string> = {
'/api/customers': 'customer:list',
'/api/customer-departments': 'customer:department',
'/api/customer-accounts': 'customer:account',
'/api/customer-visits': 'customer:visit',
'/api/customer-buildings': 'customer:building',
'/api/customer-rooms': 'customer:room',
'/api/survey-orders': 'project:survey',
'/api/project-plans': 'project:plan',
'/api/contracts': 'project:contract',
'/api/acceptances': 'project:acceptance',
'/api/visit-records': 'project:visit',
};
// 超级管理员角色编码,拥有全部权限
+121
View File
@@ -0,0 +1,121 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
import { Context } from "@midwayjs/koa";
import Acceptance from "../entity/acceptance.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
@Provide()
export class AcceptanceService {
@InjectRepository(Acceptance)
acceptModel!: Repository<Acceptance>;
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
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 generateAcceptNo(retry: number = 0): Promise<string> {
const now = new Date();
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
const yyyymmdd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
const prefix = `YS${yyyymmdd}`;
const last = await this.acceptModel.findOne({ where: { acceptNo: { [Op.like]: `${prefix}%` } }, order: [["accept_no", "DESC"]] });
let seq = 1;
if (last) seq = (parseInt(last.acceptNo.slice(-3), 10) || 0) + 1;
const result = `${prefix}${String(seq).padStart(3, "0")}`;
const exists = await this.acceptModel.findOne({ where: { acceptNo: result } });
if (exists) {
if (retry < 1) return this.generateAcceptNo(retry + 1);
throw new Error("验收单号生成冲突,请重试");
}
return result;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const userId = ctx.state.user.userId;
const where: any = {};
if (query.keyword) {
where[Op.or] = [{ acceptor: { [Op.like]: `%${query.keyword}%` } }, { acceptOpinion: { [Op.like]: `%${query.keyword}%` } }];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
const ids = myCustomers.map((c) => c.id);
if (ids.length === 0) return { code: 200, message: "获取项目验收列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
if (where.customerId !== undefined) {
if (!ids.includes(where.customerId)) return { code: 200, message: "获取项目验收列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
} else {
where.customerId = { [Op.in]: ids };
}
}
const { count, rows } = await this.acceptModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取项目验收列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
}
async detail(id: number, ctx: Context): Promise<any> {
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 };
return { code: 200, message: "获取项目验收详情成功", data: record.toJSON() };
}
async create(data: any, ctx: Context): Promise<any> {
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建验收", data: null };
try {
data.acceptNo = await this.generateAcceptNo();
const created = await this.acceptModel.create(data);
return { code: 200, message: "创建项目验收成功", data: created.toJSON() };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
data.acceptNo = await this.generateAcceptNo();
const created = await this.acceptModel.create(data);
return { code: 200, message: "创建项目验收成功", data: created.toJSON() };
}
throw err;
}
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.acceptModel.findByPk(id);
if (!record) return { code: 404, message: "项目验收不存在", data: null };
if (!(await this.isAdmin(ctx.state.user.userId))) {
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该验收", data: null };
delete data.customerId;
}
delete data.acceptNo;
await record.update(data);
return { code: 200, message: "更新项目验收成功", data: record.toJSON() };
}
async delete(id: number, ctx: Context): Promise<any> {
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 };
await record.destroy();
return { code: 200, message: "删除项目验收成功", data: null };
}
}
+151
View File
@@ -0,0 +1,151 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
import { Context } from "@midwayjs/koa";
import Contract from "../entity/contract.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
@Provide()
export class ContractService {
@InjectRepository(Contract)
contractModel!: Repository<Contract>;
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
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 generateContractNo(retry: number = 0): Promise<string> {
const now = new Date();
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
const yyyy = d.getUTCFullYear();
const yyyymmdd = `${yyyy}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
const prefix = `HT${yyyymmdd}`;
const last = await this.contractModel.findOne({ where: { contractNo: { [Op.like]: `${prefix}%` } }, order: [["contract_no", "DESC"]] });
let seq = 1;
if (last) seq = (parseInt(last.contractNo.slice(-3), 10) || 0) + 1;
const result = `${prefix}${String(seq).padStart(3, "0")}`;
const exists = await this.contractModel.findOne({ where: { contractNo: result } });
if (exists) {
if (retry < 1) return this.generateContractNo(retry + 1);
throw new Error("合同单号生成冲突,请重试");
}
return result;
}
private async generateXtContractNo(): Promise<string> {
const now = new Date();
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
const prefix = `XT${d.getUTCFullYear()}`;
const last = await this.contractModel.findOne({ where: { xtContractNo: { [Op.like]: `${prefix}%` } }, order: [["xt_contract_no", "DESC"]] });
let seq = 1;
if (last) {
const lastNo = last.xtContractNo ?? "";
const seqStr = lastNo.slice(-4);
seq = (parseInt(seqStr, 10) || 0) + 1;
}
return `${prefix}${String(seq).padStart(4, "0")}`;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const userId = ctx.state.user.userId;
const where: any = {};
if (query.keyword) {
where[Op.or] = [{ 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 (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
const ids = myCustomers.map((c) => c.id);
if (ids.length === 0) return { code: 200, message: "获取项目合同列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
if (where.customerId !== undefined) {
if (!ids.includes(where.customerId)) return { code: 200, message: "获取项目合同列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
} else {
where.customerId = { [Op.in]: ids };
}
delete where.salesmanId;
}
const { count, rows } = await this.contractModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取项目合同列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
}
async detail(id: number, ctx: Context): Promise<any> {
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 };
return { code: 200, message: "获取项目合同详情成功", data: record.toJSON() };
}
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 };
if (!(await this.isAdmin(userId))) data.salesmanId = userId;
try {
data.contractNo = await this.generateContractNo();
if (!data.xtContractNo) data.xtContractNo = await this.generateXtContractNo();
const created = await this.contractModel.create(data);
return { code: 200, message: "创建项目合同成功", data: created.toJSON() };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
data.contractNo = await this.generateContractNo();
const created = await this.contractModel.create(data);
return { code: 200, message: "创建项目合同成功", data: created.toJSON() };
}
throw err;
}
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.contractModel.findByPk(id);
if (!record) return { code: 404, message: "项目合同不存在", data: null };
const isAdmin = await this.isAdmin(ctx.state.user.userId);
if (!isAdmin) {
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该合同", data: null };
delete data.salesmanId;
delete data.customerId;
}
delete data.contractNo;
await record.update(data);
return { code: 200, message: "更新项目合同成功", data: record.toJSON() };
}
async delete(id: number, ctx: Context): Promise<any> {
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 };
await record.destroy();
return { code: 200, message: "删除项目合同成功", data: null };
}
async findOptions(customerId: number): Promise<any> {
const where: any = {};
if (customerId) where.customerId = customerId;
const records = await this.contractModel.findAll({ where, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取项目合同选项成功", data: records.map((r) => r.toJSON()) };
}
}
@@ -0,0 +1,98 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op } from "sequelize";
import { Context } from "@midwayjs/koa";
import CustomerBuilding from "../entity/customer_building.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
@Provide()
export class CustomerBuildingService {
@InjectRepository(CustomerBuilding)
buildingModel!: Repository<CustomerBuilding>;
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
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;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const userId = ctx.state.user.userId;
const where: any = {};
if (query.keyword) {
where[Op.or] = [{ buildingName: { [Op.like]: `%${query.keyword}%` } }, { purpose: { [Op.like]: `%${query.keyword}%` } }];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
const ids = myCustomers.map((c) => c.id);
if (ids.length === 0) return { code: 200, message: "获取客户建筑列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
if (where.customerId !== undefined) {
if (!ids.includes(where.customerId)) return { code: 200, message: "获取客户建筑列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
} else {
where.customerId = { [Op.in]: ids };
}
}
const { count, rows } = await this.buildingModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取客户建筑列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
}
async detail(id: number, ctx: Context): Promise<any> {
const record = await this.buildingModel.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 };
return { code: 200, message: "获取客户建筑详情成功", data: record.toJSON() };
}
async create(data: any, ctx: Context): Promise<any> {
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建建筑", data: null };
const created = await this.buildingModel.create(data);
return { code: 200, message: "创建客户建筑成功", data: created.toJSON() };
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.buildingModel.findByPk(id);
if (!record) return { code: 404, message: "客户建筑不存在", data: null };
if (!(await this.isAdmin(ctx.state.user.userId))) {
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该建筑", data: null };
delete data.customerId;
}
await record.update(data);
return { code: 200, message: "更新客户建筑成功", data: record.toJSON() };
}
async delete(id: number, ctx: Context): Promise<any> {
const record = await this.buildingModel.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 };
await record.destroy();
return { code: 200, message: "删除客户建筑成功", data: null };
}
async findOptions(customerId: number): Promise<any> {
const records = await this.buildingModel.findAll({ where: { customerId }, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取客户建筑选项成功", data: records.map((r) => r.toJSON()) };
}
}
+102
View File
@@ -0,0 +1,102 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op } from "sequelize";
import { Context } from "@midwayjs/koa";
import CustomerRoom from "../entity/customer_room.entity";
import Customer from "../entity/customer.entity";
import CustomerBuilding from "../entity/customer_building.entity";
import CustomerDepartment from "../entity/customer_department.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
@Provide()
export class CustomerRoomService {
@InjectRepository(CustomerRoom)
roomModel!: Repository<CustomerRoom>;
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(CustomerBuilding)
buildingModel!: Repository<CustomerBuilding>;
@InjectRepository(CustomerDepartment)
deptModel!: Repository<CustomerDepartment>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
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;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const userId = ctx.state.user.userId;
const where: any = {};
if (query.keyword) {
where[Op.or] = [{ roomName: { [Op.like]: `%${query.keyword}%` } }, { roomCode: { [Op.like]: `%${query.keyword}%` } }];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (query.buildingId !== undefined) where.buildingId = query.buildingId;
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
const ids = myCustomers.map((c) => c.id);
if (ids.length === 0) return { code: 200, message: "获取客户房间列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
if (where.customerId !== undefined) {
if (!ids.includes(where.customerId)) return { code: 200, message: "获取客户房间列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
} else {
where.customerId = { [Op.in]: ids };
}
}
const { count, rows } = await this.roomModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取客户房间列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
}
async detail(id: number, ctx: Context): Promise<any> {
const record = await this.roomModel.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 };
return { code: 200, message: "获取客户房间详情成功", data: record.toJSON() };
}
async create(data: any, ctx: Context): Promise<any> {
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建房间", data: null };
const created = await this.roomModel.create(data);
return { code: 200, message: "创建客户房间成功", data: created.toJSON() };
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.roomModel.findByPk(id);
if (!record) return { code: 404, message: "客户房间不存在", data: null };
if (!(await this.isAdmin(ctx.state.user.userId))) {
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该房间", data: null };
delete data.customerId;
}
await record.update(data);
return { code: 200, message: "更新客户房间成功", data: record.toJSON() };
}
async delete(id: number, ctx: Context): Promise<any> {
const record = await this.roomModel.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 };
await record.destroy();
return { code: 200, message: "删除客户房间成功", data: null };
}
}
@@ -0,0 +1,130 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
import { Context } from "@midwayjs/koa";
import CustomerVisit from "../entity/customer_visit.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
@Provide()
export class CustomerVisitService {
@InjectRepository(CustomerVisit)
visitModel!: Repository<CustomerVisit>;
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
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 generateVisitNo(retry: number = 0): Promise<string> {
const now = new Date();
const offsetMs = now.getTime() + 8 * 60 * 60 * 1000;
const d = new Date(offsetMs);
const yyyymmdd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
const prefix = `BF${yyyymmdd}`;
const last = await this.visitModel.findOne({ where: { visitNo: { [Op.like]: `${prefix}%` } }, order: [["visit_no", "DESC"]] });
let seq = 1;
if (last) seq = (parseInt(last.visitNo.slice(-3), 10) || 0) + 1;
const result = `${prefix}${String(seq).padStart(3, "0")}`;
const exists = await this.visitModel.findOne({ where: { visitNo: result } });
if (exists) {
if (retry < 1) return this.generateVisitNo(retry + 1);
throw new Error("拜访单号生成冲突,请重试");
}
return result;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const userId = ctx.state.user.userId;
const where: any = {};
if (query.keyword) {
where[Op.or] = [
{ visitPerson: { [Op.like]: `%${query.keyword}%` } },
{ visitPhone: { [Op.like]: `%${query.keyword}%` } }
];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (query.salesmanId) where.salesmanId = query.salesmanId;
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
const ids = myCustomers.map((c) => c.id);
if (ids.length === 0) return { code: 200, message: "获取客户拜访列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
if (where.customerId !== undefined) {
if (!ids.includes(where.customerId)) return { code: 200, message: "获取客户拜访列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
} else {
where.customerId = { [Op.in]: ids };
}
}
const { count, rows } = await this.visitModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取客户拜访列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
}
async detail(id: number, ctx: Context): Promise<any> {
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 };
return { code: 200, message: "获取客户拜访详情成功", data: record.toJSON() };
}
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 };
if (!(await this.isAdmin(userId))) data.salesmanId = userId;
try {
data.visitNo = await this.generateVisitNo();
const created = await this.visitModel.create(data);
return { code: 200, message: "创建客户拜访成功", data: created.toJSON() };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
data.visitNo = await this.generateVisitNo();
const created = await this.visitModel.create(data);
return { code: 200, message: "创建客户拜访成功", data: created.toJSON() };
}
throw err;
}
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.visitModel.findByPk(id);
if (!record) return { code: 404, message: "客户拜访不存在", data: null };
const isAdmin = await this.isAdmin(ctx.state.user.userId);
if (!isAdmin) {
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该拜访记录", data: null };
delete data.salesmanId;
delete data.customerId;
}
delete data.visitNo;
await record.update(data);
return { code: 200, message: "更新客户拜访成功", data: record.toJSON() };
}
async delete(id: number, ctx: Context): Promise<any> {
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 };
await record.destroy();
return { code: 200, message: "删除客户拜访成功", data: null };
}
}
+154
View File
@@ -0,0 +1,154 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
import { Context } from "@midwayjs/koa";
import ProjectPlan from "../entity/project_plan.entity";
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";
@Provide()
export class ProjectPlanService {
@InjectRepository(ProjectPlan)
planModel!: Repository<ProjectPlan>;
@InjectRepository(ProjectPlanDetail)
detailModel!: Repository<ProjectPlanDetail>;
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
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 generatePlanNo(retry: number = 0): Promise<string> {
const now = new Date();
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
const yyyymmdd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
const prefix = `FA${yyyymmdd}`;
const last = await this.planModel.findOne({ where: { planNo: { [Op.like]: `${prefix}%` } }, order: [["plan_no", "DESC"]] });
let seq = 1;
if (last) seq = (parseInt(last.planNo.slice(-3), 10) || 0) + 1;
const result = `${prefix}${String(seq).padStart(3, "0")}`;
const exists = await this.planModel.findOne({ where: { planNo: result } });
if (exists) {
if (retry < 1) return this.generatePlanNo(retry + 1);
throw new Error("方案单号生成冲突,请重试");
}
return result;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const userId = ctx.state.user.userId;
const where: any = {};
if (query.keyword) {
where[Op.or] = [{ 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 (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
const ids = myCustomers.map((c) => c.id);
if (ids.length === 0) return { code: 200, message: "获取项目方案列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
if (where.customerId !== undefined) {
if (!ids.includes(where.customerId)) return { code: 200, message: "获取项目方案列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
} else {
where.customerId = { [Op.in]: ids };
}
}
const { count, rows } = await this.planModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取项目方案列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
}
async detail(id: number, ctx: Context): Promise<any> {
const record = await this.planModel.findByPk(id, { include: [this.detailModel] });
if (!record) return { code: 404, message: "项目方案不存在", data: null };
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限访问该方案", data: null };
return { code: 200, message: "获取项目方案详情成功", data: record.toJSON() };
}
async create(data: any, ctx: Context): Promise<any> {
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建方案", data: null };
const details = data.details || [];
delete data.details;
try {
data.planNo = await this.generatePlanNo();
const created = await this.planModel.create(data);
if (details.length > 0) {
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] });
return { code: 200, message: "创建项目方案成功", data: result?.toJSON() };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
data.planNo = await this.generatePlanNo();
const created = await this.planModel.create(data);
if (details.length > 0) {
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] });
return { code: 200, message: "创建项目方案成功", data: result?.toJSON() };
}
throw err;
}
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.planModel.findByPk(id);
if (!record) return { code: 404, message: "项目方案不存在", data: null };
if (!(await this.isAdmin(ctx.state.user.userId))) {
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该方案", data: null };
delete data.customerId;
}
const details = data.details;
delete data.details;
delete data.planNo;
await record.update(data);
if (details && Array.isArray(details)) {
await this.detailModel.destroy({ where: { planId: id } });
if (details.length > 0) {
await this.detailModel.bulkCreate(details.map((d: any) => ({ ...d, planId: id, customerId: record.customerId })));
}
}
const result = await this.planModel.findByPk(id, { include: [this.detailModel] });
return { code: 200, message: "更新项目方案成功", data: result?.toJSON() };
}
async delete(id: number, ctx: Context): Promise<any> {
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 };
await this.detailModel.destroy({ where: { planId: id } });
await record.destroy();
return { code: 200, message: "删除项目方案成功", data: null };
}
async findOptions(customerId: number): Promise<any> {
const where: any = {};
if (customerId) where.customerId = customerId;
const records = await this.planModel.findAll({ where, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取项目方案选项成功", data: records.map((r) => r.toJSON()) };
}
}
+122
View File
@@ -0,0 +1,122 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
import { Context } from "@midwayjs/koa";
import SurveyOrder from "../entity/survey_order.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
@Provide()
export class SurveyOrderService {
@InjectRepository(SurveyOrder)
surveyModel!: Repository<SurveyOrder>;
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
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 generateSurveyNo(retry: number = 0): Promise<string> {
const now = new Date();
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
const yyyymmdd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
const prefix = `KC${yyyymmdd}`;
const last = await this.surveyModel.findOne({ where: { surveyNo: { [Op.like]: `${prefix}%` } }, order: [["survey_no", "DESC"]] });
let seq = 1;
if (last) seq = (parseInt(last.surveyNo.slice(-3), 10) || 0) + 1;
const result = `${prefix}${String(seq).padStart(3, "0")}`;
const exists = await this.surveyModel.findOne({ where: { surveyNo: result } });
if (exists) {
if (retry < 1) return this.generateSurveyNo(retry + 1);
throw new Error("勘察单号生成冲突,请重试");
}
return result;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const userId = ctx.state.user.userId;
const where: any = {};
if (query.keyword) {
where[Op.or] = [{ surveyor: { [Op.like]: `%${query.keyword}%` } }, { remark: { [Op.like]: `%${query.keyword}%` } }];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (query.status !== undefined) where.status = query.status;
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
const ids = myCustomers.map((c) => c.id);
if (ids.length === 0) return { code: 200, message: "获取勘察派单列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
if (where.customerId !== undefined) {
if (!ids.includes(where.customerId)) return { code: 200, message: "获取勘察派单列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
} else {
where.customerId = { [Op.in]: ids };
}
}
const { count, rows } = await this.surveyModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取勘察派单列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
}
async detail(id: number, ctx: Context): Promise<any> {
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 };
return { code: 200, message: "获取勘察派单详情成功", data: record.toJSON() };
}
async create(data: any, ctx: Context): Promise<any> {
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建勘察派单", data: null };
try {
data.surveyNo = await this.generateSurveyNo();
const created = await this.surveyModel.create(data);
return { code: 200, message: "创建勘察派单成功", data: created.toJSON() };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
data.surveyNo = await this.generateSurveyNo();
const created = await this.surveyModel.create(data);
return { code: 200, message: "创建勘察派单成功", data: created.toJSON() };
}
throw err;
}
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.surveyModel.findByPk(id);
if (!record) return { code: 404, message: "勘察派单不存在", data: null };
if (!(await this.isAdmin(ctx.state.user.userId))) {
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该勘察派单", data: null };
delete data.customerId;
}
delete data.surveyNo;
await record.update(data);
return { code: 200, message: "更新勘察派单成功", data: record.toJSON() };
}
async delete(id: number, ctx: Context): Promise<any> {
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 };
await record.destroy();
return { code: 200, message: "删除勘察派单成功", data: null };
}
}
+126
View File
@@ -0,0 +1,126 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
import { Context } from "@midwayjs/koa";
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";
@Provide()
export class VisitRecordService {
@InjectRepository(VisitRecord)
visitModel!: Repository<VisitRecord>;
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
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 generateVisitNo(retry: number = 0): Promise<string> {
const now = new Date();
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
const yyyymmdd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
const prefix = `HF${yyyymmdd}`;
const last = await this.visitModel.findOne({ where: { visitNo: { [Op.like]: `${prefix}%` } }, order: [["visit_no", "DESC"]] });
let seq = 1;
if (last) seq = (parseInt(last.visitNo.slice(-3), 10) || 0) + 1;
const result = `${prefix}${String(seq).padStart(3, "0")}`;
const exists = await this.visitModel.findOne({ where: { visitNo: result } });
if (exists) {
if (retry < 1) return this.generateVisitNo(retry + 1);
throw new Error("回访单号生成冲突,请重试");
}
return result;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const userId = ctx.state.user.userId;
const where: any = {};
if (query.keyword) {
where[Op.or] = [
{ visitor: { [Op.like]: `%${query.keyword}%` } },
{ contactPerson: { [Op.like]: `%${query.keyword}%` } },
{ contactPhone: { [Op.like]: `%${query.keyword}%` } },
{ customerOpinion: { [Op.like]: `%${query.keyword}%` } }
];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
const ids = myCustomers.map((c) => c.id);
if (ids.length === 0) return { code: 200, message: "获取客户回访列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
if (where.customerId !== undefined) {
if (!ids.includes(where.customerId)) return { code: 200, message: "获取客户回访列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
} else {
where.customerId = { [Op.in]: ids };
}
}
const { count, rows } = await this.visitModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取客户回访列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
}
async detail(id: number, ctx: Context): Promise<any> {
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 };
return { code: 200, message: "获取客户回访详情成功", data: record.toJSON() };
}
async create(data: any, ctx: Context): Promise<any> {
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建回访记录", data: null };
try {
data.visitNo = await this.generateVisitNo();
const created = await this.visitModel.create(data);
return { code: 200, message: "创建客户回访成功", data: created.toJSON() };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
data.visitNo = await this.generateVisitNo();
const created = await this.visitModel.create(data);
return { code: 200, message: "创建客户回访成功", data: created.toJSON() };
}
throw err;
}
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.visitModel.findByPk(id);
if (!record) return { code: 404, message: "客户回访不存在", data: null };
if (!(await this.isAdmin(ctx.state.user.userId))) {
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该回访记录", data: null };
delete data.customerId;
}
delete data.visitNo;
await record.update(data);
return { code: 200, message: "更新客户回访成功", data: record.toJSON() };
}
async delete(id: number, ctx: Context): Promise<any> {
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 };
await record.destroy();
return { code: 200, message: "删除客户回访成功", data: null };
}
}