feat: 多用户权限体系、AI 助手、SKU 目录与合同清洗(开源准备)
本地长期开发成果的整体入库快照,包含后端鉴权/权限、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
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 系统标识与组织名称(单一事实来源)。
|
||||
*
|
||||
* 口径(Wayne 2026-09-15 定,别再改回去):
|
||||
* - **系统名是作者固定署名,不可修改**:登录页、顶栏、浏览器标题一律显示
|
||||
* SYSTEM.name,且**不跟随组织名** —— 这套系统会被别人拿去部署,
|
||||
* 把牌子固定在作者名下是刻意保留的来源标识(引流)。
|
||||
* - **组织名(公司名)可以改**:它只是业务字段,决定「同组织」的资源边界、
|
||||
* 以及权限组/用户下拉里的显示;改组织名**不会**影响界面上的系统名。
|
||||
* - 之前那套 `settings['brand.name'] / brand.subtitle` 覆盖已作废,
|
||||
* 历史遗留的这两个键即使还在库里也不再被读取。
|
||||
*/
|
||||
|
||||
const db = require('../db/db');
|
||||
|
||||
/** 系统标识:作者固定署名,界面一律用它;不读 settings,不跟随组织名 */
|
||||
const SYSTEM = {
|
||||
name: 'happy出口通',
|
||||
subtitle: 'Ai Export System'
|
||||
};
|
||||
|
||||
/** 第一个组织视为「主组织」:开箱数据只有一个,组织名以它为准 */
|
||||
function primaryOrg() {
|
||||
try {
|
||||
return db.prepare('SELECT id, name, code, description FROM organizations ORDER BY id LIMIT 1').get() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给前端用的标识信息。
|
||||
* 故意做得轻:登录页、顶栏、浏览器标题都会调它。
|
||||
* name / subtitle 恒为固定值;org_* 只是顺带给设置页展示「当前公司名」。
|
||||
*/
|
||||
function getBranding() {
|
||||
const org = primaryOrg();
|
||||
return {
|
||||
/* 界面牌子:固定 */
|
||||
name: SYSTEM.name,
|
||||
subtitle: SYSTEM.subtitle,
|
||||
/* 前端据此把「系统标识」渲染成只读、不提供输入框 */
|
||||
system_locked: true,
|
||||
/* 公司/组织名单独给出:设置页要能看见它当前是什么(这个是可改的) */
|
||||
org_name: org?.name || '',
|
||||
org_id: org?.id ?? null
|
||||
};
|
||||
}
|
||||
|
||||
/** 改组织名(顺带把 code / 描述也支持了,组织与品牌设置页要用) */
|
||||
function updateOrg(id, { name, code, description } = {}) {
|
||||
const row = db.prepare('SELECT * FROM organizations WHERE id = ?').get(Number(id));
|
||||
if (!row) throw Object.assign(new Error('组织不存在'), { statusCode: 400 });
|
||||
|
||||
const nextName = name === undefined ? row.name : String(name || '').trim();
|
||||
if (!nextName) throw Object.assign(new Error('组织名称不能为空'), { statusCode: 400 });
|
||||
if (nextName.length > 40) throw Object.assign(new Error('组织名称不能超过 40 个字'), { statusCode: 400 });
|
||||
|
||||
const clash = db.prepare('SELECT id FROM organizations WHERE name = ? AND id <> ?').get(nextName, row.id);
|
||||
if (clash) throw Object.assign(new Error('已存在同名组织'), { statusCode: 400 });
|
||||
|
||||
db.prepare('UPDATE organizations SET name = ?, code = ?, description = ? WHERE id = ?').run(
|
||||
nextName,
|
||||
code === undefined ? row.code : (String(code || '').trim() || null),
|
||||
description === undefined ? row.description : (String(description || '').trim() || null),
|
||||
row.id
|
||||
);
|
||||
return db.prepare('SELECT id, name, code, description FROM organizations WHERE id = ?').get(row.id);
|
||||
}
|
||||
|
||||
module.exports = { SYSTEM, getBranding, updateOrg, primaryOrg };
|
||||
Reference in New Issue
Block a user