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:
Drsakura
2026-09-17 14:53:02 +08:00
parent 436519e0a6
commit 13d67c804f
77 changed files with 18407 additions and 955 deletions
+103
View File
@@ -0,0 +1,103 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import axios from 'axios'
/**
* 会话上下文:全局唯一的「当前用户是谁」答案来源。
*
* 为什么不把用户信息塞进 localStorage:
* 改了个人资料 / 换了角色之后,前端必须立刻拿到新值。所以只依赖后端的会话 cookie,
* 启动时打一次 /api/auth/me,之后由各处的 setUser / refresh 回写。
*/
const AuthContext = createContext(null)
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
/* 启动时确定登录态 */
useEffect(() => {
let alive = true
;(async () => {
try {
const { data } = await axios.get('/api/auth/me')
if (alive) setUser(data?.user || null)
} catch {
if (alive) setUser(null)
} finally {
if (alive) setLoading(false)
}
})()
return () => { alive = false }
}, [])
/* 任何接口返回 401(会话过期 / 被踢),就地清空登录态,界面自然回登录页 */
useEffect(() => {
const id = axios.interceptors.response.use(
response => response,
error => {
const status = error?.response?.status
const url = String(error?.config?.url || '')
const isAuthProbe = url.includes('/api/auth/login') || url.includes('/api/auth/me')
if (status === 401 && !isAuthProbe) setUser(null)
return Promise.reject(error)
}
)
return () => axios.interceptors.response.eject(id)
}, [])
const refresh = useCallback(async () => {
try {
const { data } = await axios.get('/api/auth/me')
setUser(data?.user || null)
return data?.user || null
} catch {
setUser(null)
return null
}
}, [])
const login = useCallback(async (username, password) => {
const { data } = await axios.post('/api/auth/login', { username, password })
setUser(data.user)
return data.user
}, [])
const logout = useCallback(async () => {
try {
await axios.post('/api/auth/logout')
} catch { /* 会话可能已过期,忽略 */ }
setUser(null)
}, [])
const value = useMemo(
() => ({ user, loading, login, logout, refresh, setUser }),
[user, loading, login, logout, refresh]
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth 必须在 AuthProvider 内使用')
return context
}
/** 权限判断:系统管理员兜底全通过,其余按角色所属权限组的权限点判断 */
export function hasPermission(user, key) {
if (!user) return false
if (user.role === 'admin') return true
return Array.isArray(user.permissions) && user.permissions.includes(key)
}
/** 角色英文值 → 中文标签 */
export const ROLE_LABELS = {
admin: '系统管理员',
chairman: '董事长',
general_manager: '总经理',
sales_manager: '销售经理',
sales_rep: '销售业务员'
}
export function roleLabel(role) {
return ROLE_LABELS[role] || role || '—'
}