feat: 新增客户管理全模块功能
实现客户信息、客户部门、客户账号三个子模块的完整CRUD,包含: 1. 数据库实体、DTO、控制器、服务层代码 2. RBAC权限映射与菜单种子数据扩展 3. 前端路由与API封装 4. 数据权限隔离与鉴权逻辑 5. 配套工具脚本与测试验证清单
This commit is contained in:
1 parent
026ec34471
commit
1c80a24d1d
31 files changed
+26321
-653
No files matched your search
@@ -0,0 +1,24 @@
|
||||
// 解码 JWT token 看 payload
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const token = process.argv[2];
|
||||
if (!token) {
|
||||
console.error('请提供 token 作为参数');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const decoded = jwt.decode(token);
|
||||
console.log('=== Token Payload ===');
|
||||
console.log(JSON.stringify(decoded, null, 2));
|
||||
|
||||
// 用 config 里的 secret 尝试验证
|
||||
const secret = 'smart-grid-iot-admin-jwt-secret-key-2024';
|
||||
try {
|
||||
const verified = jwt.verify(token, secret);
|
||||
console.log('\n=== Verify 成功 ===');
|
||||
console.log(JSON.stringify(verified, null, 2));
|
||||
} catch (err) {
|
||||
console.log('\n=== Verify 失败 ===');
|
||||
console.log('Error name:', err.name);
|
||||
console.log('Error message:', err.message);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user