Files
backmanager-server/db.js
2026-06-24 22:44:14 +08:00

382 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// db.js —— 只做两件事:导出连接池;启动时建库/建表/插 admin
require('dotenv').config() //自动寻找.env文件并把里面的键值对全部导入进来
const mysql = require('mysql2/promise')
const bcrypt = require('bcryptjs') //用来给密码做哈希加密
// ============ 1. 连接池 ============
// 业务代码里用 pool.query() / pool.execute(),会自动从池里取/还连接
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT) || 3306,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true, // 池里没连接时排队等
connectionLimit: 10, // 最多 10 个连接
decimalNumbers: true,
})
// ============ 2. 启动时初始化 ============
// 思路:先用一个"无 database"的连接建库,再回到 pool 建表、插 admin
async function initDB() {
const { DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME } = process.env
// 2.1 建库(如果不存在)
const conn = await mysql.createConnection({
host: DB_HOST,
port: Number(DB_PORT) || 3306,
user: DB_USER,
password: DB_PASSWORD,
})
await conn.query(
`CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` DEFAULT CHARACTER SET utf8mb4`
)
await conn.end()
// 2.2 建表(如果不存在)
await pool.query(`
CREATE TABLE IF NOT EXISTS users (
id INT NOT NULL AUTO_INCREMENT COMMENT '用户ID (主键)',
username VARCHAR(50) NOT NULL COMMENT '用户名 (登录账号)',
password VARCHAR(255) NOT NULL COMMENT '登录密码 (应存储加密后的值)',
is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用',
role_id INT DEFAULT NULL COMMENT '角色ID (关联roles表)',
employee_id INT DEFAULT NULL COMMENT '员工ID (关联employees表)',
department VARCHAR(100) DEFAULT NULL COMMENT '所属部门 (从employees同步的冗余字段)',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'
`)
await pool.query(`
CREATE TABLE IF NOT EXISTS customers (
id INT NOT NULL AUTO_INCREMENT COMMENT '加盟商ID (主键)',
name VARCHAR(100) NOT NULL COMMENT '加盟商姓名',
phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
province VARCHAR(50) DEFAULT NULL COMMENT '省',
city VARCHAR(50) DEFAULT NULL COMMENT '市',
district VARCHAR(50) DEFAULT NULL COMMENT '区',
address VARCHAR(200) DEFAULT NULL COMMENT '详细地址',
customer_type VARCHAR(20) DEFAULT 'Normal' COMMENT '客户类型: VIP-地区总代理, Normal-普通代理',
email VARCHAR(100) DEFAULT NULL COMMENT '电子邮箱',
remark TEXT DEFAULT NULL COMMENT '备注信息',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_customers_name (name),
KEY idx_customers_phone (phone)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='加盟商信息表'
`)
// 确保 customer_type 列存在(兼容旧表)
try { await pool.query(`ALTER TABLE customers ADD COLUMN customer_type VARCHAR(20) DEFAULT 'Normal' COMMENT '客户类型: VIP-地区总代理, Normal-普通代理' AFTER address`) } catch {}
await pool.query(`
CREATE TABLE IF NOT EXISTS employees (
id INT NOT NULL AUTO_INCREMENT COMMENT '员工ID (主键)',
name VARCHAR(100) NOT NULL COMMENT '员工姓名',
gender VARCHAR(4) DEFAULT NULL COMMENT '性别: 男/女',
age INT DEFAULT NULL COMMENT '年龄',
education VARCHAR(50) DEFAULT NULL COMMENT '学历: 高中/专科/本科/硕士/博士',
department VARCHAR(100) DEFAULT NULL COMMENT '所属部门',
entry_date DATE DEFAULT NULL COMMENT '入职时间',
position VARCHAR(100) DEFAULT NULL COMMENT '职务/岗位',
salary DECIMAL(10,2) DEFAULT NULL COMMENT '工资金额',
phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
email VARCHAR(100) DEFAULT NULL COMMENT '电子邮箱',
status TINYINT(1) NOT NULL DEFAULT 1 COMMENT '在职状态: 0-离职, 1-在职',
remark TEXT DEFAULT NULL COMMENT '备注',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_employees_name (name),
KEY idx_employees_department (department)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工信息表'
`)
await pool.query(`
CREATE TABLE IF NOT EXISTS products (
id INT NOT NULL AUTO_INCREMENT COMMENT '产品ID (主键)',
name VARCHAR(200) NOT NULL COMMENT '产品名称',
type VARCHAR(100) DEFAULT NULL COMMENT '产品类型/分类',
quantity INT NOT NULL DEFAULT 0 COMMENT '产品库存数量',
price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '产品单价',
unit VARCHAR(20) DEFAULT '件' COMMENT '计量单位',
specification VARCHAR(200) DEFAULT NULL COMMENT '产品规格/型号',
supplier VARCHAR(200) DEFAULT NULL COMMENT '供应商',
remark TEXT DEFAULT NULL COMMENT '备注',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_products_name (name),
KEY idx_products_type (type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品信息表'
`)
// contracts 依赖 customers 和 employeesafter_sales 同样依赖
await pool.query(`
CREATE TABLE IF NOT EXISTS contracts (
id INT NOT NULL AUTO_INCREMENT COMMENT '合同ID (主键)',
customer_id INT NOT NULL COMMENT '客户ID (关联客户表)',
contract_name VARCHAR(200) NOT NULL COMMENT '合同名称',
contract_no VARCHAR(100) DEFAULT NULL COMMENT '合同编号',
contract_content TEXT DEFAULT NULL COMMENT '合同内容/条款',
amount DECIMAL(12,2) DEFAULT NULL COMMENT '合同金额',
effective_date DATE DEFAULT NULL COMMENT '合同生效日期',
expiry_date DATE DEFAULT NULL COMMENT '合同有效期 (截止日期)',
employee_id INT DEFAULT NULL COMMENT '业务员ID (关联员工表)',
status VARCHAR(20) NOT NULL DEFAULT '生效' COMMENT '合同状态: 草稿/生效/完成/作废',
remark TEXT DEFAULT NULL COMMENT '备注',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_contracts_customer_id (customer_id),
KEY idx_contracts_employee_id (employee_id),
KEY idx_contracts_effective_date (effective_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合同信息表'
`)
await pool.query(`
CREATE TABLE IF NOT EXISTS after_sales (
id INT NOT NULL AUTO_INCREMENT COMMENT '售后记录ID (主键)',
customer_id INT NOT NULL COMMENT '客户ID (关联客户表)',
feedback TEXT NOT NULL COMMENT '客户反馈意见/售后内容',
employee_id INT DEFAULT NULL COMMENT '处理业务员ID (关联员工表)',
handle_method TEXT DEFAULT NULL COMMENT '处理方式/解决方案',
handle_status VARCHAR(20) NOT NULL DEFAULT '待处理' COMMENT '处理状态: 待处理/处理中/已完成',
service_date DATE DEFAULT NULL COMMENT '售后日期',
remark TEXT DEFAULT NULL COMMENT '备注',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_after_sales_customer_id (customer_id),
KEY idx_after_sales_employee_id (employee_id),
KEY idx_after_sales_status (handle_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='售后信息表'
`)
console.log('[init] 数据表初始化完成')
// ============ 2.3 RBAC 权限相关表 ============
await pool.query(`
CREATE TABLE IF NOT EXISTS roles (
id INT NOT NULL AUTO_INCREMENT COMMENT '角色ID (主键)',
name VARCHAR(50) NOT NULL COMMENT '角色标识 (英文)',
description VARCHAR(200) DEFAULT NULL COMMENT '角色描述 (中文)',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_roles_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'
`)
await pool.query(`
CREATE TABLE IF NOT EXISTS permissions (
id INT NOT NULL AUTO_INCREMENT COMMENT '权限ID (主键)',
name VARCHAR(100) NOT NULL COMMENT '权限标识 (resource:action)',
description VARCHAR(200) DEFAULT NULL COMMENT '权限描述 (中文)',
resource VARCHAR(50) NOT NULL COMMENT '资源名称',
action VARCHAR(50) NOT NULL COMMENT '操作类型: read/create/update/delete',
PRIMARY KEY (id),
UNIQUE KEY uk_permissions_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表'
`)
await pool.query(`
CREATE TABLE IF NOT EXISTS role_permissions (
role_id INT NOT NULL COMMENT '角色ID',
permission_id INT NOT NULL COMMENT '权限ID',
PRIMARY KEY (role_id, permission_id),
KEY idx_rp_permission_id (permission_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色-权限关联表'
`)
await pool.query(`
CREATE TABLE IF NOT EXISTS suppliers (
id INT NOT NULL AUTO_INCREMENT COMMENT '供应商ID (主键)',
name VARCHAR(200) NOT NULL COMMENT '供应商名称',
contact VARCHAR(100) DEFAULT NULL COMMENT '联系人',
phone VARCHAR(50) DEFAULT NULL COMMENT '联系电话',
address VARCHAR(500) DEFAULT NULL COMMENT '地址',
type VARCHAR(50) DEFAULT NULL COMMENT '类型: 原材料/包装/设备',
content TEXT DEFAULT NULL COMMENT '备注说明',
status TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态: 0-停用, 1-正常',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_suppliers_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商信息表'
`)
// ============ 2.4 旧表迁移(兼容已有数据库) ============
// users 表:删除 real_name、role新增 is_active、role_id、employee_id、department
try { await pool.query(`ALTER TABLE users DROP COLUMN real_name`) } catch {}
try { await pool.query(`ALTER TABLE users DROP COLUMN role`) } catch {}
try { await pool.query(`ALTER TABLE users CHANGE COLUMN status is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用'`) } catch {}
try { await pool.query(`ALTER TABLE users ADD COLUMN role_id INT DEFAULT NULL COMMENT '角色ID' AFTER is_active`) } catch {}
try { await pool.query(`ALTER TABLE users ADD COLUMN employee_id INT DEFAULT NULL COMMENT '员工ID' AFTER role_id`) } catch {}
try { await pool.query(`ALTER TABLE users ADD COLUMN department VARCHAR(100) DEFAULT NULL COMMENT '所属部门' AFTER employee_id`) } catch {}
// customers 表:新增 responsible_user_id
try { await pool.query(`ALTER TABLE customers ADD COLUMN responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID'`) } catch {}
// contracts 表:新增 type
try { await pool.query(`ALTER TABLE contracts ADD COLUMN type VARCHAR(20) DEFAULT 'franchise' COMMENT '合同类型: franchise-加盟合同, supply-采购合同'`) } catch {}
// contracts 表:新增 responsible_user_id用于数据范围隔离
try { await pool.query(`ALTER TABLE contracts ADD COLUMN responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID'`) } catch {}
// after_sales 表:新增 responsible_user_id用于数据范围隔离
try { await pool.query(`ALTER TABLE after_sales ADD COLUMN responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID'`) } catch {}
// ============ 2.5 种子数据 ============
await seedRolesAndPermissions()
await seedDefaultUsers()
}
// ============ 3. 种子数据:角色与权限 ============
async function seedRolesAndPermissions() {
const [roles] = await pool.query('SELECT id FROM roles LIMIT 1')
if (roles.length > 0) return // 已初始化过,跳过
const roleList = [
{ name: 'admin', description: '系统管理员' },
{ name: 'general_manager', description: '总经理' },
{ name: 'franchise_manager', description: '招商经理' },
{ name: 'operations_manager', description: '运营经理' },
{ name: 'procurement_manager', description: '采购经理' },
{ name: 'finance', description: '财务人员' },
]
for (const r of roleList) {
await pool.query('INSERT INTO roles (name, description) VALUES (?, ?)', [r.name, r.description])
}
console.log('[init] 已初始化 6 个角色')
const permissionList = [
{ name: 'customer:read', description: '查看加盟商', resource: 'customer', action: 'read' },
{ name: 'customer:create', description: '新增加盟商', resource: 'customer', action: 'create' },
{ name: 'customer:update', description: '修改加盟商', resource: 'customer', action: 'update' },
{ name: 'customer:delete', description: '删除加盟商', resource: 'customer', action: 'delete' },
{ name: 'contract:read', description: '查看合同', resource: 'contract', action: 'read' },
{ name: 'contract:create', description: '新增合同', resource: 'contract', action: 'create' },
{ name: 'contract:update', description: '修改合同', resource: 'contract', action: 'update' },
{ name: 'contract:delete', description: '删除合同', resource: 'contract', action: 'delete' },
{ name: 'after_sale:read', description: '查看售后', resource: 'after_sale', action: 'read' },
{ name: 'after_sale:create',description: '新增售后', resource: 'after_sale', action: 'create' },
{ name: 'after_sale:update',description: '修改售后', resource: 'after_sale', action: 'update' },
{ name: 'after_sale:delete',description: '删除售后', resource: 'after_sale', action: 'delete' },
{ name: 'product:read', description: '查看产品原料', resource: 'product', action: 'read' },
{ name: 'product:create', description: '新增产品原料', resource: 'product', action: 'create' },
{ name: 'product:update', description: '修改产品原料', resource: 'product', action: 'update' },
{ name: 'product:delete', description: '删除产品原料', resource: 'product', action: 'delete' },
{ name: 'supplier:read', description: '查看供应商', resource: 'supplier', action: 'read' },
{ name: 'supplier:create', description: '新增供应商', resource: 'supplier', action: 'create' },
{ name: 'supplier:update', description: '修改供应商', resource: 'supplier', action: 'update' },
{ name: 'supplier:delete', description: '删除供应商', resource: 'supplier', action: 'delete' },
{ name: 'employee:read', description: '查看员工', resource: 'employee', action: 'read' },
{ name: 'employee:create', description: '新增员工', resource: 'employee', action: 'create' },
{ name: 'employee:update', description: '修改员工', resource: 'employee', action: 'update' },
{ name: 'employee:delete', description: '删除员工', resource: 'employee', action: 'delete' },
{ name: 'user:manage', description: '管理用户账号', resource: 'user', action: 'manage' },
]
for (const p of permissionList) {
await pool.query(
'INSERT INTO permissions (name, description, resource, action) VALUES (?, ?, ?, ?)',
[p.name, p.description, p.resource, p.action]
)
}
console.log('[init] 已初始化 25 个权限')
// 角色-权限映射
const allPerms = permissionList.map(p => p.name)
const rolePermMap = {
admin: allPerms, // 管理员拥有所有权限
general_manager: [
'customer:read', 'contract:read', 'after_sale:read',
'product:read', 'supplier:read', 'employee:read',
],
franchise_manager: [
'customer:read', 'customer:create', 'customer:update', 'customer:delete',
'contract:read', 'contract:create', 'contract:update', 'contract:delete',
'employee:read',
],
operations_manager: [
'customer:read', 'customer:update',
'contract:read',
'after_sale:read', 'after_sale:create', 'after_sale:update', 'after_sale:delete',
'employee:read',
],
procurement_manager: [
'contract:read', 'contract:create', 'contract:update', 'contract:delete',
'product:read', 'product:create', 'product:update', 'product:delete',
'supplier:read', 'supplier:create', 'supplier:update', 'supplier:delete',
'employee:read',
],
finance: [
'customer:read', 'contract:read', 'after_sale:read', 'employee:read',
],
}
const [roleRows] = await pool.query('SELECT id, name FROM roles')
const [permRows] = await pool.query('SELECT id, name FROM permissions')
const roleIdMap = Object.fromEntries(roleRows.map(r => [r.name, r.id]))
const permIdMap = Object.fromEntries(permRows.map(p => [p.name, p.id]))
for (const [roleName, permNames] of Object.entries(rolePermMap)) {
for (const permName of permNames) {
await pool.query(
'INSERT IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)',
[roleIdMap[roleName], permIdMap[permName]]
)
}
}
console.log('[init] 已初始化角色-权限映射')
}
// ============ 4. 种子数据:默认用户 ============
async function seedDefaultUsers() {
const hash = await bcrypt.hash('123456', 10)
const [roleRows] = await pool.query('SELECT id, name FROM roles')
const roleIdMap = Object.fromEntries(roleRows.map(r => [r.name, r.id]))
const seedEmployees = [
{ name: '张超', gender: '男', age: 42, education: '硕士', department: '总经理办公室', entry_date: '2015-03-01', position: '总经理', salary: 50000, phone: '13800001001', email: 'zhangchao@mixue.com' },
{ name: '李明', gender: '男', age: 35, education: '本科', department: '招商部', entry_date: '2018-06-15', position: '招商经理', salary: 18000, phone: '13800001002', email: 'liming@mixue.com' },
{ name: '王丽', gender: '女', age: 33, education: '本科', department: '运营部', entry_date: '2019-01-10', position: '运营经理', salary: 18000, phone: '13800001003', email: 'wangli@mixue.com' },
{ name: '赵强', gender: '男', age: 36, education: '本科', department: '采购部', entry_date: '2017-09-20', position: '采购经理', salary: 18000, phone: '13800001004', email: 'zhaoqiang@mixue.com' },
{ name: '陈芳', gender: '女', age: 30, education: '本科', department: '财务部', entry_date: '2020-03-05', position: '财务主管', salary: 15000, phone: '13800001005', email: 'chenfang@mixue.com' },
{ name: '系统管理员', gender: '男', age: 28, education: '本科', department: '信息技术部', entry_date: '2022-07-01', position: '系统管理员', salary: 12000, phone: '13800001006', email: 'admin@mixue.com' },
]
const seedUsers = [
{ username: 'zhangchao', roleName: 'general_manager' },
{ username: 'liming', roleName: 'franchise_manager' },
{ username: 'wangli', roleName: 'operations_manager' },
{ username: 'zhaoqiang', roleName: 'procurement_manager' },
{ username: 'chenfang', roleName: 'finance' },
{ username: 'admin', roleName: 'admin' },
]
for (let i = 0; i < seedUsers.length; i++) {
const emp = seedEmployees[i]
const usr = seedUsers[i]
// 逐个检查用户是否存在(不依赖全局检查,避免部分用户被误删后跳过全部重建)
const [userCheck] = await pool.query('SELECT id FROM users WHERE username = ?', [usr.username])
if (userCheck.length > 0) continue
// 先插入 employees
const [empResult] = await pool.query(
`INSERT INTO employees (name, gender, age, education, department, entry_date, position, salary, phone, email, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
[emp.name, emp.gender, emp.age, emp.education, emp.department, emp.entry_date, emp.position, emp.salary, emp.phone, emp.email]
)
const empId = empResult.insertId
// 再插入 users关联 employee 和 role
await pool.query(
`INSERT INTO users (username, password, is_active, role_id, employee_id, department)
VALUES (?, ?, 1, ?, ?, ?)`,
[usr.username, hash, roleIdMap[usr.roleName], empId, emp.department]
)
console.log(`[init] 已创建用户 ${usr.username} (${emp.name} - ${emp.department})`)
}
}
module.exports = { pool, initDB }