本地长期开发成果的整体入库快照,包含后端鉴权/权限、AI 接入、合同解析、 SKU 目录等模块,以及前端相应页面与样式改版。 后端 - 新增鉴权与会话:scrypt 密码哈希、HttpOnly cookie(happy_sid)、登录失败锁定 - 新增「角色 → 权限组 → 权限点」三级权限模型与审计日志 - 新增 AI 提供商接入与预设(兼容 OpenAI 协议,内置 DeepSeek / 通义千问等) - 新增合同解析、合同文件保留策略、客户归属与访问控制 - 新增 SKU 目录、组织品牌、新客开发、系统日志等路由 前端 - 新增登录页与用户 / 权限组 / 系统日志管理页 - 新增客户详情、客户公海、询盘详情、报价编辑器、合同清洗中心 - 新增 AI 配置、组织品牌、包装参数、SKU 选择器等组件 - 新增英文大写金额、未保存改动拦截、抽屉与骨架屏 - 新增 stage1 样式层;产品、供应商、订单等页面改版 - EmbeddedSku 页面并入现有产品体系后删除 开源准备 - 新增 AGPL-3.0 LICENSE - README:移除未启用的 SQLCipher 加密声明(改为如实的「已知限制」)、 补充环境变量说明、许可证由「私有项目」改为 AGPL-3.0 - 移除历史遗留的空文件 _index_backup.txt - 文档中的项目路径更新为 happy-export
92 lines
3.6 KiB
JavaScript
92 lines
3.6 KiB
JavaScript
/**
|
|
* 客户归属与可见性口径(唯一事实来源)。
|
|
*
|
|
* Wayne 2026-09-14 定的规矩:
|
|
* ① 客户有「所属」:谁建的归谁,默认只有归属人自己看得见;
|
|
* ② 创建人主动丢进公海(pool_status = 'public')后,同组织成员都能看见、都能领取;
|
|
* ③ 其余情况只有拿到 `customer.view_all` 的最高级别管理账号能查看。
|
|
*
|
|
* 「能看」与「能改」是两档:
|
|
* 公海里的客户同组织成员都能看见(不然没法判断值不值得领),
|
|
* 但只有归属人或管理账号能改 —— 别人想改得先「领取」。
|
|
*
|
|
* ⚠️ 注意 req.user 是 users 表的原始行(attachUser 直接塞的是 sessionUser 的结果),
|
|
* 上面**没有** permissions 字段,权限必须用 auth.permissionsOfRole(role) 现推。
|
|
*/
|
|
const auth = require('./auth');
|
|
|
|
/** 越过归属看全部客户的权限点 */
|
|
const VIEW_ALL = 'customer.view_all';
|
|
|
|
/** 是否属于「最高级别系统管理账号」 */
|
|
function seesAll(user) {
|
|
if (!user || !user.role) return false;
|
|
return auth.permissionsOfRole(user.role).includes(VIEW_ALL);
|
|
}
|
|
|
|
/**
|
|
* 列表可见性 → 可直接拼进 WHERE 的条件数组 + 参数。
|
|
*
|
|
* scope:
|
|
* 'mine' 我的客户(默认)—— owner_id = 我
|
|
* 'all' 全部客户 —— 需 customer.view_all,无权限时退回 'mine' 而不是报错
|
|
* 'pool' 客户公海 —— pool_status = 'public',且限定本组织
|
|
*
|
|
* 调用方拼 SQL 时,customers 表必须别名为 **c**。
|
|
*/
|
|
function scopeFilter(user, scope = 'mine') {
|
|
const conditions = [];
|
|
const params = [];
|
|
|
|
if (scope === 'pool') {
|
|
conditions.push("c.pool_status = 'public'");
|
|
if (user && user.org_id) {
|
|
/* 公海是「组织(团队)」的资源,只放本组织的进来。
|
|
存量没 org_id 的老数据不设门槛,免得刚升级就凭空少一批。 */
|
|
conditions.push('(c.org_id IS NULL OR c.org_id = ?)');
|
|
params.push(user.org_id);
|
|
}
|
|
return { conditions, params };
|
|
}
|
|
|
|
if (scope === 'all' && seesAll(user)) {
|
|
return { conditions, params };
|
|
}
|
|
|
|
/* 「我的客户」= 归我 + 还没交出去。
|
|
丢进公海后这条就从我的列表里移走,只留在「客户公海」——
|
|
否则用户会疑惑「我都丢出去了怎么还在我这儿」。 */
|
|
conditions.push('c.owner_id = ?');
|
|
conditions.push("COALESCE(c.pool_status, 'owned') <> 'public'");
|
|
params.push(user ? user.id : -1);
|
|
return { conditions, params };
|
|
}
|
|
|
|
/** 公海是「组织(团队)」的资源:两边都记了组织且不一致,就是外人 */
|
|
function inSameOrg(user, customer) {
|
|
const mine = user?.org_id;
|
|
const theirs = customer?.org_id;
|
|
/* 存量数据可能没组织,不设门槛 —— 与 scopeFilter 里 pool 的写法保持一致 */
|
|
if (!mine || !theirs) return true;
|
|
return Number(mine) === Number(theirs);
|
|
}
|
|
|
|
/** 能不能看这条客户(详情页、跟进时间线) */
|
|
function canView(user, customer) {
|
|
if (!customer) return false;
|
|
if (seesAll(user)) return true;
|
|
if (user && Number(customer.owner_id) === Number(user.id)) return true;
|
|
/* 公海客户同组织可见;别组织的公海客户连详情都不该看到 */
|
|
if (customer.pool_status === 'public' && inSameOrg(user, customer)) return true;
|
|
return false;
|
|
}
|
|
|
|
/** 能不能改这条客户(基本信息、联系人、跟进记录) */
|
|
function canEdit(user, customer) {
|
|
if (!customer) return false;
|
|
if (seesAll(user)) return true;
|
|
return !!user && Number(customer.owner_id) === Number(user.id);
|
|
}
|
|
|
|
module.exports = { VIEW_ALL, seesAll, scopeFilter, inSameOrg, canView, canEdit };
|