Files
Drsakura 13d67c804f 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
2026-09-17 14:53:02 +08:00

72 lines
3.1 KiB
JavaScript

/**
* 系统标识与组织名称(单一事实来源)。
*
* 口径(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 };