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:
@@ -51,6 +51,11 @@ CREATE TABLE IF NOT EXISTS products (
|
||||
last_updated TEXT,
|
||||
source_contract TEXT,
|
||||
confidence TEXT DEFAULT 'high',
|
||||
-- 上传者 / 归属组织 / 撞号副本来源:产品库是组织共享的,谁都能传自己那一版,
|
||||
-- 「谁传的、复制自谁」必须可追,见 lib/skuCatalog.js 的 insertItem。
|
||||
uploader TEXT,
|
||||
org_id INTEGER,
|
||||
duplicated_from TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT
|
||||
);
|
||||
@@ -110,6 +115,8 @@ CREATE TABLE IF NOT EXISTS price_history (
|
||||
supplier_id INTEGER REFERENCES suppliers(id),
|
||||
source_contract TEXT,
|
||||
confidence TEXT,
|
||||
-- 谁改的价:报价单里的采购价要能追到出处,和 insertItem 的手工留痕同源
|
||||
changed_by TEXT,
|
||||
changed_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
@@ -142,6 +149,39 @@ CREATE TABLE IF NOT EXISTS product_images (
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
/* 小推车:产品页里挑出来的货号暂存区,出报价单前的中转台。
|
||||
全局唯一一份(不按用户分车)—— 这是当前口径,改成分人分车要同时动前端。 */
|
||||
CREATE TABLE IF NOT EXISTS cart_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sku TEXT NOT NULL UNIQUE,
|
||||
qty INTEGER,
|
||||
note TEXT,
|
||||
sort INTEGER DEFAULT 0,
|
||||
added_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 二进制存储:图片与合同原件存进库,不摊在磁盘上。
|
||||
* 单独成表而不是给 product_images / contracts 加 BLOB 列 —— 那两张表都用
|
||||
* SELECT * 查,把二进制挂在同一行会让每次列表查询都把整张图读进内存。
|
||||
* --------------------------------------------------------------------------- */
|
||||
CREATE TABLE IF NOT EXISTS product_image_files (
|
||||
image_id INTEGER PRIMARY KEY REFERENCES product_images(id),
|
||||
bytes BLOB NOT NULL,
|
||||
size INTEGER,
|
||||
stored_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contract_files (
|
||||
contract_id INTEGER PRIMARY KEY REFERENCES contracts(id),
|
||||
filename TEXT,
|
||||
mime TEXT,
|
||||
size INTEGER,
|
||||
sha256 TEXT,
|
||||
bytes BLOB NOT NULL,
|
||||
stored_at TEXT
|
||||
);
|
||||
|
||||
-- ================== 客户管理(CRM) ==================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS customers (
|
||||
@@ -156,7 +196,17 @@ CREATE TABLE IF NOT EXISTS customers (
|
||||
email TEXT,
|
||||
rating TEXT,
|
||||
status TEXT DEFAULT 'active',
|
||||
/* 开发结果:pending 未成交 / won 已成交 / lost 已流失(新客开发页按此分栏) */
|
||||
deal_status TEXT DEFAULT 'pending',
|
||||
/* 归属:谁建的归谁,默认只有归属人看得见;创建人主动丢进公海才对同组织开放 */
|
||||
owner_id INTEGER REFERENCES users(id),
|
||||
/* created_by 单独记「谁建的」—— 公海被领取后 owner_id 会换人,创建人这一笔仍要留痕 */
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
org_id INTEGER REFERENCES organizations(id),
|
||||
/* pool_status:owned 名下有主 / public 已丢入公海(同组织可见、可领取) */
|
||||
pool_status TEXT DEFAULT 'owned',
|
||||
pool_at TEXT,
|
||||
pool_note TEXT,
|
||||
source TEXT,
|
||||
tags TEXT,
|
||||
notes TEXT,
|
||||
@@ -275,10 +325,20 @@ CREATE TABLE IF NOT EXISTS customer_quotations (
|
||||
quote_date TEXT NOT NULL,
|
||||
valid_until TEXT,
|
||||
currency TEXT DEFAULT 'USD',
|
||||
-- 条款结构化字段:payment_terms / delivery_terms 仍保留可读文本,供老页面直接展示
|
||||
pi_no TEXT,
|
||||
payment_method TEXT,
|
||||
deposit_percent REAL,
|
||||
balance_percent REAL,
|
||||
delivery_method TEXT,
|
||||
delivery_port TEXT,
|
||||
validity_days INTEGER,
|
||||
payment_terms TEXT,
|
||||
delivery_terms TEXT,
|
||||
status TEXT DEFAULT 'draft',
|
||||
notes TEXT,
|
||||
-- 系统备注:只给内部看,报价单对外不显示
|
||||
internal_notes TEXT,
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT
|
||||
@@ -295,6 +355,10 @@ CREATE TABLE IF NOT EXISTS quotation_items (
|
||||
unit_price REAL NOT NULL,
|
||||
total_price REAL NOT NULL,
|
||||
currency TEXT DEFAULT 'USD',
|
||||
-- 成本口径快照:报价单发出去之后,供应商再调价也不能让这张单的历史毛利变样
|
||||
purchase_price REAL,
|
||||
exchange_rate REAL,
|
||||
markup_rate REAL,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
@@ -339,20 +403,63 @@ CREATE TABLE IF NOT EXISTS prospect_leads (
|
||||
|
||||
-- ================== 系统管理 ==================
|
||||
|
||||
-- 组织(团队):产品/客户的归属单位,「同组织的人均可参考」以它为边界。
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
code TEXT,
|
||||
description TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
email TEXT,
|
||||
/* role = 权限角色(董事长 / 总经理 / 销售经理 / 销售业务员 …),也是「角色 → 权限组」映射的键 */
|
||||
role TEXT DEFAULT 'user',
|
||||
/* position = 对外展示的职位称谓,纯文字,与角色解耦 */
|
||||
position TEXT,
|
||||
phone TEXT,
|
||||
address TEXT,
|
||||
avatar TEXT,
|
||||
org_id INTEGER REFERENCES organizations(id),
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT,
|
||||
last_login_at TEXT,
|
||||
failed_count INTEGER NOT NULL DEFAULT 0,
|
||||
locked_until TEXT
|
||||
);
|
||||
|
||||
-- 权限组:一套命名的权限范围。角色被指派进某个组,即获得该组的全部权限点。
|
||||
CREATE TABLE IF NOT EXISTS permission_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
is_system INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
-- 权限组 → 权限点(permission_key 由后端 PERMISSIONS 清单定义)
|
||||
CREATE TABLE IF NOT EXISTS permission_group_permissions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
group_id INTEGER NOT NULL REFERENCES permission_groups(id),
|
||||
permission_key TEXT NOT NULL,
|
||||
UNIQUE (group_id, permission_key)
|
||||
);
|
||||
|
||||
-- 角色 → 权限组(一个角色同时只属于一个组,改指派即换组)
|
||||
CREATE TABLE IF NOT EXISTS role_permission_groups (
|
||||
role TEXT PRIMARY KEY,
|
||||
group_id INTEGER NOT NULL REFERENCES permission_groups(id),
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
@@ -401,3 +508,125 @@ CREATE INDEX IF NOT EXISTS idx_prospect_leads_run ON prospect_leads(run_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_exp ON sessions(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_at ON audit_log(at);
|
||||
|
||||
-- ================== 询盘与报价主链路 ==================
|
||||
CREATE TABLE IF NOT EXISTS inquiries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
inquiry_no TEXT NOT NULL UNIQUE,
|
||||
customer_id INTEGER NOT NULL REFERENCES customers(id),
|
||||
inquiry_date TEXT NOT NULL,
|
||||
title TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
currency TEXT DEFAULT 'USD',
|
||||
source TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inquiry_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
inquiry_id INTEGER NOT NULL REFERENCES inquiries(id),
|
||||
sku TEXT NOT NULL,
|
||||
product_name TEXT,
|
||||
supplier_id INTEGER REFERENCES suppliers(id),
|
||||
supplier_name TEXT,
|
||||
purchase_price REAL NOT NULL,
|
||||
currency TEXT NOT NULL DEFAULT 'CNY',
|
||||
exchange_rate REAL NOT NULL DEFAULT 1,
|
||||
markup_rate REAL NOT NULL DEFAULT 0,
|
||||
markup_factor REAL NOT NULL DEFAULT 1,
|
||||
exw REAL NOT NULL,
|
||||
quantity INTEGER NOT NULL DEFAULT 1,
|
||||
packaging_snapshot TEXT,
|
||||
product_snapshot TEXT,
|
||||
quoted_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS follow_up_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
customer_id INTEGER NOT NULL REFERENCES customers(id),
|
||||
activity_type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
next_action TEXT,
|
||||
next_action_date TEXT,
|
||||
contact_id INTEGER REFERENCES contacts(id),
|
||||
inquiry_id INTEGER REFERENCES inquiries(id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_packaging (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sku TEXT NOT NULL UNIQUE REFERENCES products(sku),
|
||||
inner_pack_qty INTEGER,
|
||||
carton_qty INTEGER,
|
||||
carton_length REAL,
|
||||
carton_width REAL,
|
||||
carton_height REAL,
|
||||
carton_weight REAL,
|
||||
unit_length REAL,
|
||||
unit_width REAL,
|
||||
unit_height REAL,
|
||||
unit_weight REAL,
|
||||
packaging_material TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contract_import_jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_no TEXT NOT NULL UNIQUE,
|
||||
status TEXT NOT NULL DEFAULT 'queued',
|
||||
file_count INTEGER NOT NULL DEFAULT 0,
|
||||
source_files TEXT,
|
||||
parse_method TEXT NOT NULL DEFAULT 'rules',
|
||||
rule_version TEXT NOT NULL DEFAULT 'mvp-v1',
|
||||
-- column_map:本次认列结果(字段 → 列号/表头/得分),出问题时唯一能自证的现场
|
||||
column_map TEXT,
|
||||
-- parse_source:这份结果是怎么来的 —— rules(字符规则)/ ai(模型解析)
|
||||
parse_source TEXT DEFAULT 'rules',
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT,
|
||||
completed_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contract_import_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_id INTEGER NOT NULL REFERENCES contract_import_jobs(id),
|
||||
filename TEXT NOT NULL,
|
||||
row_no INTEGER,
|
||||
raw_text TEXT,
|
||||
sku TEXT,
|
||||
product_name TEXT,
|
||||
spec TEXT,
|
||||
purchase_price REAL,
|
||||
currency TEXT,
|
||||
quantity INTEGER,
|
||||
moq INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'pending_review',
|
||||
review_note TEXT,
|
||||
parse_source TEXT DEFAULT 'rules',
|
||||
reviewed_by INTEGER REFERENCES users(id),
|
||||
reviewed_at TEXT,
|
||||
-- committed_at / product_sku:入库留痕 —— 什么时候进的库、进成了哪个货号
|
||||
-- (撞号时实际落库的是 -COPY2,光看 sku 列看不出来)
|
||||
committed_at TEXT,
|
||||
product_sku TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inquiries_customer ON inquiries(customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_inquiries_status_date ON inquiries(status, inquiry_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_inquiry_items_inquiry ON inquiry_items(inquiry_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_inquiry_items_sku ON inquiry_items(sku);
|
||||
CREATE INDEX IF NOT EXISTS idx_follow_up_customer_date ON follow_up_records(customer_id, next_action_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_follow_up_inquiry ON follow_up_records(inquiry_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_product_packaging_sku ON product_packaging(sku);
|
||||
CREATE INDEX IF NOT EXISTS idx_contract_import_jobs_status ON contract_import_jobs(status, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_contract_import_items_job ON contract_import_items(job_id, status);
|
||||
|
||||
Reference in New Issue
Block a user