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,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())
};
}
}