feat: 新增客户管理全模块功能

实现客户信息、客户部门、客户账号三个子模块的完整CRUD,包含:
1. 数据库实体、DTO、控制器、服务层代码
2. RBAC权限映射与菜单种子数据扩展
3. 前端路由与API封装
4. 数据权限隔离与鉴权逻辑
5. 配套工具脚本与测试验证清单
This commit is contained in:
andyesfly committed 2026-08-23 19:15:54 +08:00
1 parent 026ec34471
commit 1c80a24d1d
31 files changed
+26321 -653

No files matched your search

@@ -0,0 +1,54 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { CustomerAccountService } from "../service/customer.account.service";
@Controller("/api/customer-accounts")
export class CustomerAccountController {
@Inject()
ctx!: Context;
@Inject()
customerAccountService!: CustomerAccountService;
@Get("/")
async list(@Query() query: any) {
const result = await this.customerAccountService.list(query, this.ctx);
return result;
}
@Get("/:id")
async detail(@Param("id") id: string) {
const result = await this.customerAccountService.detail(Number(id), this.ctx);
return result;
}
@Post("/")
async create(@Body() body: any) {
const result = await this.customerAccountService.create(body, this.ctx);
return result;
}
@Put("/:id")
async update(@Param("id") id: string, @Body() body: any) {
const result = await this.customerAccountService.update(Number(id), body, this.ctx);
return result;
}
@Del("/:id")
async delete(@Param("id") id: string) {
const result = await this.customerAccountService.delete(Number(id), this.ctx);
return result;
}
@Put("/:id/status")
async updateStatus(@Param("id") id: string, @Body() body: { status: number }) {
const result = await this.customerAccountService.updateStatus(Number(id), body.status, this.ctx);
return result;
}
@Post("/:id/reset-password")
async resetPassword(@Param("id") id: string) {
const result = await this.customerAccountService.resetPassword(Number(id), this.ctx);
return result;
}
}
@@ -0,0 +1,54 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { CustomerService } from "../service/customer.service";
@Controller("/api/customers")
export class CustomerController {
@Inject()
ctx!: Context;
@Inject()
customerService!: CustomerService;
@Get("/")
async list(@Query() query: any) {
const result = await this.customerService.list(query, this.ctx);
return result;
}
@Get("/options/all")
async getSimpleList() {
const result = await this.customerService.getSimpleList(this.ctx);
return result;
}
@Get("/:id")
async detail(@Param("id") id: string) {
const result = await this.customerService.detail(Number(id), this.ctx);
return result;
}
@Post("/")
async create(@Body() body: any) {
const result = await this.customerService.create(body, this.ctx);
return result;
}
@Put("/:id")
async update(@Param("id") id: string, @Body() body: any) {
const result = await this.customerService.update(Number(id), body, this.ctx);
return result;
}
@Del("/:id")
async delete(@Param("id") id: string) {
const result = await this.customerService.delete(Number(id), this.ctx);
return result;
}
@Put("/:id/status")
async updateStatus(@Param("id") id: string, @Body() body: { status: number }) {
const result = await this.customerService.updateStatus(Number(id), body.status, this.ctx);
return result;
}
}
@@ -0,0 +1,42 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { CustomerDepartmentService } from "../service/customer.department.service";
@Controller("/api/customer-departments")
export class CustomerDepartmentController {
@Inject()
ctx!: Context;
@Inject()
customerDepartmentService!: CustomerDepartmentService;
@Get("/")
async list(@Query() query: any) {
const result = await this.customerDepartmentService.list(query, this.ctx);
return result;
}
@Get("/:id")
async detail(@Param("id") id: string) {
const result = await this.customerDepartmentService.detail(Number(id), this.ctx);
return result;
}
@Post("/")
async create(@Body() body: any) {
const result = await this.customerDepartmentService.create(body, this.ctx);
return result;
}
@Put("/:id")
async update(@Param("id") id: string, @Body() body: any) {
const result = await this.customerDepartmentService.update(Number(id), body, this.ctx);
return result;
}
@Del("/:id")
async delete(@Param("id") id: string) {
const result = await this.customerDepartmentService.delete(Number(id), this.ctx);
return result;
}
}
+60
View File
@@ -0,0 +1,60 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, IsIn, Matches, Min, Max, MaxLength } from "class-validator";
export class CustomerAccountQueryDTO {
@IsOptional()
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@IsInt()
@Min(1)
@Max(200, { message: '分页大小不能超过 200 条' })
pageSize?: number = 10;
@IsOptional()
@IsString()
@MaxLength(32)
keyword?: string;
@IsOptional()
@IsInt()
customerId?: number;
@IsOptional()
@IsIn([0, 1])
status?: number;
}
export class CustomerAccountCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' })
@IsInt()
customerId!: number;
@IsNotEmpty({ message: '手机号不能为空' })
@Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' })
phone!: string;
@IsOptional()
@IsIn([0, 1])
status?: number;
}
export class CustomerAccountUpdateDTO {
@IsOptional()
@IsIn([0, 1])
status?: number;
@IsOptional()
resetPassword?: boolean;
@IsOptional()
@Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' })
phone?: string;
}
export class CustomerAccountUpdateStatusDTO {
@IsNotEmpty({ message: '状态不能为空' })
@IsIn([0, 1])
status!: number;
}
+81
View File
@@ -0,0 +1,81 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, Matches, Min, Max, MinLength, MaxLength } from "class-validator";
export class CustomerDepartmentQueryDTO {
@IsOptional()
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@IsInt()
@Min(1)
@Max(200, { message: '分页大小不能超过 200 条' })
pageSize?: number = 10;
@IsOptional()
@IsString()
@MaxLength(64)
keyword?: string;
@IsOptional()
@IsInt()
customerId?: number;
}
export class CustomerDepartmentCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' })
@IsInt()
customerId!: number;
@IsNotEmpty({ message: '部门名称不能为空' })
@IsString()
@MinLength(2)
@MaxLength(64)
@Matches(/^[^<>]*$/, { message: '部门名称不允许包含尖括号' })
deptName!: string;
@IsOptional()
@IsString()
@MaxLength(32)
@Matches(/^[^<>]*$/, { message: '联系人不允许包含尖括号' })
contactPerson?: string;
@IsOptional()
@Matches(/^1[3-9]\d{9}$/, { message: '联系电话格式不正确' })
contactPhone?: string;
@IsOptional()
@IsString()
@MaxLength(256)
@Matches(/^[^<>]*$/, { message: '职能描述不允许包含尖括号' })
functionDesc?: string;
}
export class CustomerDepartmentUpdateDTO {
@IsOptional()
@IsInt()
customerId?: number;
@IsOptional()
@IsString()
@MinLength(2)
@MaxLength(64)
@Matches(/^[^<>]*$/, { message: '部门名称不允许包含尖括号' })
deptName?: string;
@IsOptional()
@IsString()
@MaxLength(32)
@Matches(/^[^<>]*$/, { message: '联系人不允许包含尖括号' })
contactPerson?: string;
@IsOptional()
@Matches(/^1[3-9]\d{9}$/, { message: '联系电话格式不正确' })
contactPhone?: string;
@IsOptional()
@IsString()
@MaxLength(256)
@Matches(/^[^<>]*$/, { message: '职能描述不允许包含尖括号' })
functionDesc?: string;
}
+153
View File
@@ -0,0 +1,153 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, IsIn, Matches, Min, Max, MinLength, MaxLength } from "class-validator";
export class CustomerQueryDTO {
@IsOptional()
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@IsInt()
@Min(1)
@Max(200, { message: '分页大小不能超过 200 条' })
pageSize?: number = 10;
@IsOptional()
@IsString()
@MaxLength(64, { message: '关键词最大 64 字符' })
keyword?: string;
@IsOptional()
@IsIn([1, 2, 3, 4], { message: '客户状态值非法' })
status?: number;
@IsOptional()
@IsInt()
salesmanId?: number;
}
export class CustomerCreateDTO {
@IsNotEmpty({ message: '客户名称不能为空' })
@IsString()
@MinLength(2)
@MaxLength(128)
@Matches(/^[^<>]*$/, { message: '客户名称不允许包含尖括号' })
customerName!: string;
@IsOptional()
@IsString()
@MaxLength(32)
province?: string;
@IsOptional()
@IsString()
@MaxLength(32)
city?: string;
@IsOptional()
@IsString()
@MaxLength(32)
district?: string;
@IsOptional()
@IsString()
@MaxLength(256)
@Matches(/^[^<>]*$/, { message: '地址不允许包含尖括号' })
address?: string;
@IsOptional()
@IsString()
@MaxLength(32)
@Matches(/^[^<>]*$/, { message: '联系人不允许包含尖括号' })
contactPerson?: string;
@IsOptional()
@Matches(/^1[3-9]\d{9}$/, { message: '联系电话格式不正确' })
contactPhone?: string;
@IsOptional()
@Matches(/^\d{0,10}$/, { message: '邮编格式不正确' })
postalCode?: string;
@IsOptional()
@IsString()
@MaxLength(32)
fax?: string;
@IsOptional()
@IsString()
@MaxLength(32)
@Matches(/^[^<>]*$/, { message: '客户级别不允许包含尖括号' })
customerLevel?: string;
@IsOptional()
@IsIn([1, 2, 3, 4])
status?: number;
@IsOptional()
@IsInt()
salesmanId?: number;
}
export class CustomerUpdateDTO {
@IsOptional()
@IsString()
@MinLength(2)
@MaxLength(128)
@Matches(/^[^<>]*$/, { message: '客户名称不允许包含尖括号' })
customerName?: string;
@IsOptional()
@IsString()
@MaxLength(32)
province?: string;
@IsOptional()
@IsString()
@MaxLength(32)
city?: string;
@IsOptional()
@IsString()
@MaxLength(32)
district?: string;
@IsOptional()
@IsString()
@MaxLength(256)
@Matches(/^[^<>]*$/, { message: '地址不允许包含尖括号' })
address?: string;
@IsOptional()
@IsString()
@MaxLength(32)
@Matches(/^[^<>]*$/, { message: '联系人不允许包含尖括号' })
contactPerson?: string;
@IsOptional()
@Matches(/^1[3-9]\d{9}$/, { message: '联系电话格式不正确' })
contactPhone?: string;
@IsOptional()
@Matches(/^\d{0,10}$/, { message: '邮编格式不正确' })
postalCode?: string;
@IsOptional()
@IsString()
@MaxLength(32)
fax?: string;
@IsOptional()
@IsString()
@MaxLength(32)
@Matches(/^[^<>]*$/, { message: '客户级别不允许包含尖括号' })
customerLevel?: string;
@IsOptional()
@IsIn([1, 2, 3, 4])
status?: number;
@IsOptional()
@IsInt()
salesmanId?: number;
}
+126
View File
@@ -0,0 +1,126 @@
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, HasMany } from "sequelize-typescript";
import CustomerDepartment from "./customer_department.entity";
import CustomerAccount from "./customer_account.entity";
@Table({
tableName: "customer",
comment: "客户信息表",
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
indexes: [
{ name: "idx_customer_name", fields: ["customer_name"] },
{ name: "idx_customer_salesman", fields: ["salesman_id"] },
{ name: "idx_customer_status", fields: ["status"] },
{ unique: true, name: "uk_customer_no", fields: ["customer_no"] }
]
})
export default class Customer extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
primaryKey: true
})
id!: number;
@Column({
type: DataType.STRING(64),
allowNull: false,
unique: true,
field: "customer_no",
comment: "客户业务编号(KH+YYYYMMDD+序号)"
})
customerNo!: string;
@Column({
type: DataType.STRING(128),
allowNull: false,
comment: "客户名称"
})
customerName!: string;
@Column({
type: DataType.STRING(32),
comment: "省"
})
province!: string;
@Column({
type: DataType.STRING(32),
comment: "市"
})
city!: string;
@Column({
type: DataType.STRING(32),
comment: "区"
})
district!: string;
@Column({
type: DataType.STRING(256),
comment: "详细地址"
})
address!: string;
@Column({
type: DataType.STRING(32),
field: "contact_person",
comment: "联系人"
})
contactPerson!: string;
@Column({
type: DataType.STRING(20),
field: "contact_phone",
comment: "联系电话"
})
contactPhone!: string;
@Column({
type: DataType.STRING(10),
field: "postal_code",
comment: "邮编"
})
postalCode!: string;
@Column({
type: DataType.STRING(32),
comment: "传真"
})
fax!: string;
@Column({
type: DataType.STRING(32),
field: "customer_level",
comment: "客户级别(学校/医院/企业等)"
})
customerLevel!: string;
@Column({
type: DataType.SMALLINT,
allowNull: false,
defaultValue: 1,
comment: "状态:1意向/2签约/3运维中/4终止"
})
status!: number;
@Column({
type: DataType.BIGINT,
field: "salesman_id",
comment: "负责业务员(关联sys_user.id),references->sys_user.id"
})
salesmanId!: number;
@HasMany(() => CustomerDepartment, { foreignKey: "customerId" })
customerDepartments!: CustomerDepartment[];
@HasMany(() => CustomerAccount, { foreignKey: "customerId" })
customerAccounts!: CustomerAccount[];
@CreatedAt
created_at!: Date;
@UpdatedAt
updated_at!: Date;
}
@@ -0,0 +1,78 @@
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, BelongsTo, ForeignKey } from "sequelize-typescript";
import Customer from "./customer.entity";
@Table({
tableName: "customer_account",
comment: "客户访问账号表",
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
indexes: [
{ name: "idx_account_customer", fields: ["customer_id"] },
{ unique: true, name: "uk_account_phone", fields: ["phone"] }
]
})
export default class CustomerAccount 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(20),
allowNull: false,
unique: true,
comment: "登录手机号(账号)"
})
phone!: string;
@Column({
type: DataType.STRING(256),
allowNull: false,
field: "password_hash",
comment: "bcrypt密码哈希"
})
passwordHash!: string;
@Column({
type: DataType.STRING(128),
field: "wx_openid",
comment: "微信openid"
})
wxOpenid!: string;
@Column({
type: DataType.SMALLINT,
allowNull: false,
defaultValue: 1,
comment: "状态:0禁用/1启用"
})
status!: number;
@Column({
type: DataType.DATE,
field: "last_login_at",
comment: "最后登录时间"
})
lastLoginAt!: Date;
@BelongsTo(() => Customer)
customer!: Customer;
@CreatedAt
created_at!: Date;
@UpdatedAt
updated_at!: Date;
}
@@ -0,0 +1,68 @@
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, BelongsTo, ForeignKey } from "sequelize-typescript";
import Customer from "./customer.entity";
@Table({
tableName: "customer_department",
comment: "客户部门表",
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
indexes: [
{ name: "idx_dept_customer", fields: ["customer_id"] }
]
})
export default class CustomerDepartment 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(64),
allowNull: false,
field: "dept_name",
comment: "部门名称"
})
deptName!: string;
@Column({
type: DataType.STRING(32),
field: "contact_person",
comment: "联系人"
})
contactPerson!: string;
@Column({
type: DataType.STRING(20),
field: "contact_phone",
comment: "联系电话"
})
contactPhone!: string;
@Column({
type: DataType.STRING(256),
field: "function_desc",
comment: "职能描述"
})
functionDesc!: string;
@BelongsTo(() => Customer)
customer!: Customer;
@CreatedAt
created_at!: Date;
@UpdatedAt
updated_at!: Date;
}
+12 -10
View File
@@ -26,17 +26,9 @@ export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
const token = authHeader.substring(7);
let decoded: any;
try {
const decoded = this.tokenUtil.verifyToken(token);
ctx.state.user = {
userId: decoded.userId,
username: decoded.username,
};
ctx.user = ctx.state.user;
await next();
decoded = this.tokenUtil.verifyToken(token);
} catch (error: any) {
if (error.name === 'TokenExpiredError') {
ctx.status = 401;
@@ -52,7 +44,17 @@ export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
message: '无效的认证令牌',
};
}
return;
}
ctx.state.user = {
userId: decoded.userId,
username: decoded.username,
};
ctx.user = ctx.state.user;
await next();
};
}
}
+3
View File
@@ -13,6 +13,9 @@ const PERMISSION_MAP: Record<string, string> = {
'/api/menus': 'system:menu',
'/api/dicts': 'system:dict',
'/api/logs': 'system:log',
'/api/customers': 'customer:list',
'/api/customer-departments': 'customer:department',
'/api/customer-accounts': 'customer:account',
};
// 超级管理员角色编码,拥有全部权限
@@ -0,0 +1,343 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op } from "sequelize";
import CustomerAccount from "../entity/customer_account.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { hashPassword } from "../util/crypto";
import { Context } from "@midwayjs/koa";
function getDefaultPassword(phone: string): string {
return phone.length >= 6 ? phone.slice(-6) : "123456";
}
@Provide()
export class CustomerAccountService {
@InjectRepository(CustomerAccount)
accountModel!: Repository<CustomerAccount>;
@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 SysUserRole.findAll({
where: { userId },
include: [SysRole]
});
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 Customer.findByPk(customerId);
if (!customer) {
return false;
}
return customer.salesmanId === userId;
}
private cleanSensitiveFields(obj: any): any {
const result = obj.toJSON ? obj.toJSON() : { ...obj };
delete result.passwordHash;
delete result.wxOpenid;
return result;
}
async list(query: { page?: any; pageSize?: any; keyword?: string; customerId?: number; status?: number }, 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.phone = { [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 myCustomerIds = myCustomers.map((c) => c.id);
if (myCustomerIds.length === 0) {
return {
code: 200,
message: "获取客户账号列表成功",
data: {
list: [],
total: 0,
page: Number(page),
pageSize: Number(pageSize)
}
};
}
if (where.customerId !== undefined) {
if (!myCustomerIds.includes(where.customerId)) {
return {
code: 200,
message: "获取客户账号列表成功",
data: {
list: [],
total: 0,
page: Number(page),
pageSize: Number(pageSize)
}
};
}
} else {
where.customerId = { [Op.in]: myCustomerIds };
}
}
const { count, rows } = await this.accountModel.findAndCountAll({
where,
limit: pageSize,
offset: (page - 1) * pageSize,
order: [["created_at", "DESC"]]
});
const list = rows.map((row) => this.cleanSensitiveFields(row));
return {
code: 200,
message: "获取客户账号列表成功",
data: {
list,
total: count,
page: Number(page),
pageSize: Number(pageSize)
}
};
}
async detail(id: number, ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const record = await this.accountModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户账号不存在",
data: null
};
}
if (!(await this.isCustomerOwner(userId, record.customerId))) {
return {
code: 403,
message: "无权限访问该账号",
data: null
};
}
return {
code: 200,
message: "获取客户账号详情成功",
data: this.cleanSensitiveFields(record)
};
}
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 existing = await this.accountModel.findOne({
where: { phone: data.phone }
});
if (existing) {
return {
code: 400,
message: "手机号已存在",
data: null
};
}
const password = getDefaultPassword(data.phone);
const passwordHash = await hashPassword(password);
const record = await this.accountModel.create({
customerId: data.customerId,
phone: data.phone,
passwordHash,
status: data.status ?? 1
});
return {
code: 200,
message: "创建客户账号成功",
data: this.cleanSensitiveFields(record)
};
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const record = await this.accountModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户账号不存在",
data: null
};
}
if (!(await this.isCustomerOwner(userId, record.customerId))) {
return {
code: 403,
message: "无权限修改该账号",
data: null
};
}
if (data.resetPassword === true) {
const password = getDefaultPassword(record.phone);
record.passwordHash = await hashPassword(password);
await record.save();
}
if (data.phone !== undefined && data.phone !== record.phone) {
const conflict = await this.accountModel.findOne({
where: { phone: data.phone }
});
if (conflict) {
return {
code: 400,
message: "手机号已存在",
data: null
};
}
}
const cleanedData: any = {};
if (data.phone !== undefined) cleanedData.phone = data.phone;
if (data.status !== undefined) cleanedData.status = data.status;
const isAdmin = await this.isAdmin(userId);
if (isAdmin && data.customerId !== undefined) {
cleanedData.customerId = data.customerId;
}
await record.update(cleanedData);
return {
code: 200,
message: "更新客户账号成功",
data: this.cleanSensitiveFields(record)
};
}
async updateStatus(id: number, status: number, ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const record = await this.accountModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户账号不存在",
data: null
};
}
if (!(await this.isCustomerOwner(userId, record.customerId))) {
return {
code: 403,
message: "无权限修改该账号状态",
data: null
};
}
await record.update({ status });
return {
code: 200,
message: "状态更新成功",
data: this.cleanSensitiveFields(record)
};
}
async delete(id: number, ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const record = await this.accountModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户账号不存在",
data: null
};
}
if (!(await this.isCustomerOwner(userId, record.customerId))) {
return {
code: 403,
message: "无权限删除该账号",
data: null
};
}
await record.destroy();
return {
code: 200,
message: "删除客户账号成功",
data: null
};
}
async resetPassword(id: number, ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const record = await this.accountModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户账号不存在",
data: null
};
}
if (!(await this.isCustomerOwner(userId, record.customerId))) {
return {
code: 403,
message: "无权限重置该账号密码",
data: null
};
}
const password = getDefaultPassword(record.phone);
record.passwordHash = await hashPassword(password);
await record.save();
return {
code: 200,
message: "重置密码成功",
data: this.cleanSensitiveFields(record)
};
}
}
@@ -0,0 +1,237 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op } from "sequelize";
import CustomerDepartment from "../entity/customer_department.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { Context } from "@midwayjs/koa";
@Provide()
export class CustomerDepartmentService {
@InjectRepository(CustomerDepartment)
depModel!: Repository<CustomerDepartment>;
@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 SysUserRole.findAll({
where: { userId },
include: [SysRole]
});
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 Customer.findByPk(customerId);
if (!customer) {
return false;
}
return customer.salesmanId === userId;
}
async list(query: { page?: any; pageSize?: any; keyword?: string; customerId?: number }, 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] = [
{ deptName: { [Op.like]: `%${query.keyword}%` } },
{ contactPerson: { [Op.like]: `%${query.keyword}%` } },
{ contactPhone: { [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 myCustomerIds = myCustomers.map((c) => c.id);
if (myCustomerIds.length === 0) {
return {
code: 200,
message: "获取客户部门列表成功",
data: {
list: [],
total: 0,
page: Number(page),
pageSize: Number(pageSize)
}
};
}
if (where.customerId !== undefined) {
if (!myCustomerIds.includes(where.customerId)) {
return {
code: 200,
message: "获取客户部门列表成功",
data: {
list: [],
total: 0,
page: Number(page),
pageSize: Number(pageSize)
}
};
}
} else {
where.customerId = { [Op.in]: myCustomerIds };
}
}
const { count, rows } = await this.depModel.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 userId = ctx.state.user.userId;
const record = await this.depModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户部门不存在",
data: null
};
}
if (!(await this.isCustomerOwner(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
};
}
const record = await this.depModel.create(data);
return {
code: 200,
message: "创建客户部门成功",
data: record.toJSON()
};
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const record = await this.depModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户部门不存在",
data: null
};
}
if (!(await this.isCustomerOwner(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 userId = ctx.state.user.userId;
const record = await this.depModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户部门不存在",
data: null
};
}
if (!(await this.isCustomerOwner(userId, record.customerId))) {
return {
code: 403,
message: "无权限删除该部门",
data: null
};
}
await record.destroy();
return {
code: 200,
message: "删除客户部门成功",
data: null
};
}
async findByCustomerId(customerId: number): Promise<any> {
const records = await this.depModel.findAll({
where: { customerId },
order: [["created_at", "DESC"]]
});
return {
code: 200,
message: "获取客户部门列表成功",
data: records.map((r) => r.toJSON())
};
}
}
+321
View File
@@ -0,0 +1,321 @@
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 Customer from "../entity/customer.entity";
import CustomerDepartment from "../entity/customer_department.entity";
import CustomerAccount from "../entity/customer_account.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
@Provide()
export class CustomerService {
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(CustomerDepartment)
customerDepModel!: Repository<CustomerDepartment>;
@InjectRepository(CustomerAccount)
customerAccModel!: Repository<CustomerAccount>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
async list(
query: { page?: any; pageSize?: any; keyword?: string; status?: number; salesmanId?: number },
ctx: Context
): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const where: any = {};
if (query.keyword) {
where[Op.or] = [
{ customerName: { [Op.like]: `%${query.keyword}%` } },
{ contactPerson: { [Op.like]: `%${query.keyword}%` } },
{ contactPhone: { [Op.like]: `%${query.keyword}%` } }
];
}
if (query.status !== undefined) {
where.status = query.status;
}
if (query.salesmanId) {
where.salesmanId = query.salesmanId;
}
const isAdmin = await this.isAdmin(ctx.state.user.userId);
if (!isAdmin) {
where.salesmanId = ctx.state.user.userId;
}
const { count, rows } = await this.customerModel.findAndCountAll({
where,
limit: pageSize,
offset: (page - 1) * pageSize,
order: [["created_at", "DESC"]]
});
return {
code: 200,
message: "获取客户列表成功",
data: {
list: rows,
total: count,
page: Number(page),
pageSize: Number(pageSize)
}
};
}
async detail(id: number, ctx: Context): Promise<any> {
const record = await this.customerModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户不存在",
data: null
};
}
const isAdmin = await this.isAdmin(ctx.state.user.userId);
if (!isAdmin && record.salesmanId !== ctx.state.user.userId) {
return {
code: 403,
message: "无权限访问该客户",
data: null
};
}
return {
code: 200,
message: "获取客户详情成功",
data: record
};
}
private async isAdmin(userId: number): Promise<boolean> {
const userRoles = await this.userRoleModel.findAll({
where: { userId },
include: [SysRole]
});
return userRoles.some((ur) => {
const role = (ur as any).role as SysRole;
return role && role.roleCode === "ADMIN";
});
}
private async generateCustomerNo(retry: number = 0): Promise<string> {
const now = new Date();
const offsetMs = now.getTime() + 8 * 60 * 60 * 1000;
const dateLocal = new Date(offsetMs);
const yyyy = dateLocal.getUTCFullYear();
const mm = String(dateLocal.getUTCMonth() + 1).padStart(2, "0");
const dd = String(dateLocal.getUTCDate()).padStart(2, "0");
const yyyymmdd = `${yyyy}${mm}${dd}`;
const lastRecord = await this.customerModel.findOne({
where: { customerNo: { [Op.like]: `KH${yyyymmdd}%` } },
order: [["customer_no", "DESC"]]
});
let newSeq = 1;
if (lastRecord) {
const lastNo = lastRecord.customerNo;
const seqStr = lastNo.slice(-3);
const seq = parseInt(seqStr, 10) || 0;
newSeq = seq + 1;
}
const newSeqStr = String(newSeq).padStart(3, "0");
const result = `KH${yyyymmdd}${newSeqStr}`;
const exists = await this.customerModel.findOne({
where: { customerNo: result }
});
if (exists) {
if (retry < 1) {
return this.generateCustomerNo(retry + 1);
} else {
throw new Error("客户编号生成冲突,请重试");
}
}
return result;
}
async create(data: any, ctx: Context): Promise<any> {
const isAdmin = await this.isAdmin(ctx.state.user.userId);
if (!isAdmin) {
data.salesmanId = ctx.state.user.userId;
}
try {
data.customerNo = await this.generateCustomerNo();
const created = await this.customerModel.create(data);
return {
code: 200,
message: "创建客户成功",
data: created.toJSON()
};
} catch (err: any) {
if (err instanceof UniqueConstraintError && err.errors?.some((e) => e.path === "customer_no" || e.path === "uk_customer_no")) {
try {
data.customerNo = await this.generateCustomerNo();
const created = await this.customerModel.create(data);
return {
code: 200,
message: "创建客户成功",
data: created.toJSON()
};
} catch (_e) {
return {
code: 400,
message: "客户编号冲突,请重试",
data: null
};
}
}
throw err;
}
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.customerModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户不存在",
data: null
};
}
const isAdmin = await this.isAdmin(ctx.state.user.userId);
if (!isAdmin && record.salesmanId !== ctx.state.user.userId) {
return {
code: 403,
message: "无权限操作该客户",
data: null
};
}
delete data.customerNo;
delete data.createdAt;
delete data.updatedAt;
if (!isAdmin) {
delete data.salesmanId;
}
await record.update(data);
return {
code: 200,
message: "更新客户成功",
data: record
};
}
async delete(id: number, ctx: Context): Promise<any> {
const record = await this.customerModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户不存在",
data: null
};
}
const isAdmin = await this.isAdmin(ctx.state.user.userId);
if (!isAdmin && record.salesmanId !== ctx.state.user.userId) {
return {
code: 403,
message: "无权限操作该客户",
data: null
};
}
const hasDep = await this.customerDepModel.findOne({ where: { customerId: id }, limit: 1 });
const hasAcc = await this.customerAccModel.findOne({ where: { customerId: id }, limit: 1 });
if (hasDep || hasAcc) {
return {
code: 400,
message: "该客户存在关联的部门或账号,无法删除,请先删除关联数据",
data: null
};
}
await record.destroy();
return {
code: 200,
message: "删除客户成功",
data: null
};
}
async updateStatus(id: number, status: number, ctx: Context): Promise<any> {
const record = await this.customerModel.findByPk(id);
if (!record) {
return {
code: 404,
message: "客户不存在",
data: null
};
}
const isAdmin = await this.isAdmin(ctx.state.user.userId);
if (!isAdmin && record.salesmanId !== ctx.state.user.userId) {
return {
code: 403,
message: "无权限操作该客户",
data: null
};
}
await record.update({ status });
return {
code: 200,
message: "更新客户状态成功",
data: null
};
}
async getSimpleList(ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const isAdmin = await this.isAdmin(userId);
const where: any = {};
if (!isAdmin) {
where.salesmanId = userId;
}
const rows = await this.customerModel.findAll({
where,
attributes: ["id", "customerNo", "customerName", "status"],
order: [["created_at", "DESC"]]
});
return {
code: 200,
message: "获取客户选项成功",
data: rows.map((r) => r.toJSON())
};
}
}