docs: 重组文档结构,按模块分类
新的文档结构:
docs/
├── README.md # 文档中心导航
├── modules/ # 模块文档
│ ├── auth/ # 认证模块
│ │ ├── README.md
│ │ ├── feishu-login.md
│ │ ├── feishu-qrcode.md
│ │ └── feishu-notification.md
│ ├── approval/ # 审批流程模块
│ │ └── README.md
│ ├── business-trip/ # 出差申请模块
│ │ └── README.md
│ └── user/ # 用户管理模块
│ └── README.md
└── guides/ # 指南文档
├── deployment.md
├── environment.md
├── git-workflow.md
└── ...
每个模块都有独立的 README.md 说明概述、API、关键文件等
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
# 审批流程模块 (Approval Module)
|
||||
|
||||
## 概述
|
||||
|
||||
审批流程模块是系统的核心功能,支持多种审批节点类型和灵活的流程配置。
|
||||
|
||||
---
|
||||
|
||||
## 节点类型
|
||||
|
||||
| 类型 | 代码 | 说明 |
|
||||
|------|------|------|
|
||||
| 单人审批 | `single_approve` | 需要一个审批人通过 |
|
||||
| 会签 | `and_sign` | 所有人都需要通过 |
|
||||
| 或签 | `or_sign` | 任一人通过即可 |
|
||||
| 通知 | `notify` | 仅通知,无需审批 |
|
||||
|
||||
---
|
||||
|
||||
## 订单状态
|
||||
|
||||
| 状态 | 代码 | 说明 |
|
||||
|------|------|------|
|
||||
| 待审批 | `pending` | 等待当前节点审批 |
|
||||
| 已通过 | `approved` | 流程完成,已通过 |
|
||||
| 已拒绝 | `rejected` | 流程完成,已拒绝 |
|
||||
| 已撤销 | `cancelled` | 申请人主动撤销 |
|
||||
|
||||
---
|
||||
|
||||
## 列表过滤逻辑
|
||||
|
||||
### 我发起的
|
||||
```sql
|
||||
applicant_id = current_user_id
|
||||
```
|
||||
|
||||
### 待我审批
|
||||
```sql
|
||||
status = 'pending'
|
||||
AND current_user_id IN 当前节点审批人列表
|
||||
AND NOT EXISTS (已审批记录)
|
||||
```
|
||||
|
||||
### 我已审批
|
||||
```sql
|
||||
EXISTS (用户有审批记录)
|
||||
AND status IN ('approved', 'rejected', 'cancelled')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 接口
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/approval/list` | GET | 获取审批列表 |
|
||||
| `/api/v1/approval/detail/:id` | GET | 获取审批详情 |
|
||||
| `/api/v1/approval/approve/:id` | POST | 审批通过 |
|
||||
| `/api/v1/approval/reject/:id` | POST | 审批拒绝 |
|
||||
| `/api/v1/approval/cancel/:id` | POST | 撤销申请 |
|
||||
|
||||
---
|
||||
|
||||
## 前端关键文件
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── pages/approval/
|
||||
│ ├── list.tsx # 审批列表页
|
||||
│ └── detail.tsx # 审批详情页
|
||||
└── modules/approval/
|
||||
└── components/ # 审批相关组件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端关键文件
|
||||
|
||||
```
|
||||
backend/app/
|
||||
├── Controller/
|
||||
│ └── ApprovalController.php # 审批控制器
|
||||
├── Service/
|
||||
│ └── ApprovalService.php # 审批服务
|
||||
└── Model/
|
||||
├── ApprovalOrder.php # 审批订单
|
||||
├── ApprovalFlow.php # 审批流程
|
||||
├── ApprovalNode.php # 审批节点
|
||||
└── ApprovalRecord.php # 审批记录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### approval_order (审批订单)
|
||||
```sql
|
||||
id -- 主键
|
||||
company_id -- 公司ID
|
||||
applicant_id -- 申请人ID
|
||||
title -- 标题
|
||||
content -- 内容
|
||||
type -- 类型 (business_trip, expense, etc.)
|
||||
status -- 状态 (pending, approved, rejected, cancelled)
|
||||
current_node_id -- 当前节点ID
|
||||
created_at
|
||||
updated_at
|
||||
```
|
||||
|
||||
### approval_node (审批节点)
|
||||
```sql
|
||||
id -- 主键
|
||||
flow_id -- 流程ID
|
||||
node_type -- 节点类型 (single_approve, and_sign, or_sign, notify)
|
||||
node_order -- 节点顺序
|
||||
approvers -- 审批人列表 (JSON)
|
||||
```
|
||||
|
||||
### approval_record (审批记录)
|
||||
```sql
|
||||
id -- 主键
|
||||
order_id -- 订单ID
|
||||
node_id -- 节点ID
|
||||
approver_id -- 审批人ID
|
||||
action -- 操作 (approve, reject, cancel)
|
||||
comment -- 意见
|
||||
created_at
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 流程图
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 申请人提交 │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ 通过 ┌─────────────┐
|
||||
│ 节点1审批 │ ────────────▶│ 节点2审批 │
|
||||
└──────┬──────┘ └──────┬──────┘
|
||||
│ 拒绝 │ 拒绝
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ 已拒绝 │ │ 已拒绝 │
|
||||
└─────────────┘ └─────────────┘
|
||||
|
||||
│ 通过到最后节点
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ 已通过 │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试数据
|
||||
|
||||
运行测试数据脚本:
|
||||
```bash
|
||||
docker exec x-server-backend php test_data.php
|
||||
```
|
||||
|
||||
该脚本会创建:
|
||||
- 2 个待审批的订单(其他用户提交,用户1审批)
|
||||
- 1 个已通过的订单(用户1已审批)
|
||||
- 1 个已拒绝的订单(用户1已拒绝)
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [PROJECT_ISSUES.md](../../PROJECT_ISSUES.md) - 历史问题汇总
|
||||
- [IMPLEMENTATION_PLAN.md](../../IMPLEMENTATION_PLAN.md) - 实现计划
|
||||
|
||||
---
|
||||
|
||||
**负责人**: 待分配
|
||||
**最后更新**: 2026-02-28
|
||||
@@ -0,0 +1,137 @@
|
||||
# 认证模块 (Auth Module)
|
||||
|
||||
## 概述
|
||||
|
||||
认证模块负责用户登录、登出、token 管理等功能。支持多种登录方式:
|
||||
|
||||
| 登录方式 | 说明 | 文档 |
|
||||
|---------|------|------|
|
||||
| 飞书内免登 | 在飞书客户端内自动登录 | [feishu-login.md](./feishu-login.md) |
|
||||
| 飞书外扫码登录 | 普通浏览器扫码登录 | [feishu-qrcode.md](./feishu-qrcode.md) |
|
||||
| 邮箱密码登录 | 传统账号密码登录 | - |
|
||||
| 飞书消息通知 | 飞书卡片消息通知 | [feishu-notification.md](./feishu-notification.md) |
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 飞书免登
|
||||
|
||||
用户在飞书客户端内打开应用时,自动完成登录,无需输入密码。
|
||||
|
||||
**关键点:**
|
||||
- 使用 H5 JSSDK (`h5-js-sdk-1.5.19.js`)
|
||||
- 调用 `tt.requestAuthCode()` 获取授权码
|
||||
- 支持多租户(通过 `company` 参数)
|
||||
|
||||
### 2. Token 自动刷新
|
||||
|
||||
当 API 返回 401 时,在飞书环境中自动刷新 token,用户无感知。
|
||||
|
||||
**流程:**
|
||||
```
|
||||
API 返回 401
|
||||
↓
|
||||
检测飞书环境
|
||||
↓
|
||||
调用 tt.requestAuthCode 获取新授权码
|
||||
↓
|
||||
用新授权码登录获取新 token
|
||||
↓
|
||||
用新 token 重试原始请求
|
||||
```
|
||||
|
||||
### 3. 扫码登录
|
||||
|
||||
用户在普通浏览器中使用飞书 APP 扫码登录。
|
||||
|
||||
---
|
||||
|
||||
## API 接口
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/auth/feishu-login` | POST | 飞书免登/扫码登录 |
|
||||
| `/api/v1/auth/feishu-config` | GET | 获取公司飞书应用配置 |
|
||||
| `/api/v1/auth/qrcode-login` | POST | 生成扫码登录二维码 |
|
||||
| `/api/v1/auth/qrcode-login/status` | GET | 轮询扫码状态 |
|
||||
| `/login/callback` | GET | 飞书授权回调 |
|
||||
| `/api/v1/auth/login` | POST | 邮箱密码登录 |
|
||||
|
||||
---
|
||||
|
||||
## 前端关键文件
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── pages/login/
|
||||
│ ├── index.tsx # 登录页主逻辑
|
||||
│ └── QRCodeLogin.tsx # 扫码登录组件
|
||||
├── services/
|
||||
│ └── api.ts # API 封装(含 401 token 刷新)
|
||||
└── stores/
|
||||
└── user.ts # 用户状态管理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端关键文件
|
||||
|
||||
```
|
||||
backend/app/
|
||||
├── Controller/
|
||||
│ └── AuthController.php # 认证控制器
|
||||
├── Service/
|
||||
│ └── FeishuService.php # 飞书服务
|
||||
└── Model/
|
||||
├── User.php # 用户模型
|
||||
└── CompanyFeishuApp.php # 飞书应用配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 环境变量
|
||||
|
||||
```env
|
||||
# 飞书应用配置(多租户时在数据库中配置)
|
||||
FEISHU_APP_ID=cli_xxxxxxxxxxxxx
|
||||
FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxx
|
||||
FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxxxxxxxx
|
||||
FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## URL 参数
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `company` | 公司ID(必需) | `?company=2` |
|
||||
| `app_id` | 飞书应用ID(可选) | `?app_id=cli_xxx` |
|
||||
| `redirect` | 登录成功后跳转地址 | `?redirect=/dashboard` |
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 飞书免登失败 "invalid appId"
|
||||
**A:** 确保 URL 中有 `company` 参数,或直接传 `app_id` 参数。
|
||||
|
||||
### Q2: 扫码后没反应
|
||||
**A:** 检查飞书开放平台是否配置了正确的重定向 URL 白名单。
|
||||
|
||||
### Q3: Token 过期后需要重新登录
|
||||
**A:** 已实现自动刷新,在飞书环境中会自动获取新 token。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [飞书登录完整经验](./feishu-login.md)
|
||||
- [飞书扫码登录详解](./feishu-qrcode.md)
|
||||
- [环境配置说明](../guides/environment.md)
|
||||
|
||||
---
|
||||
|
||||
**负责人**: mzl
|
||||
**最后更新**: 2026-02-28
|
||||
@@ -0,0 +1,562 @@
|
||||
# 飞书登录实现经验总结
|
||||
|
||||
本文档记录了飞书登录(扫码登录 + 静默免登)的完整实现经验和调试过程中遇到的问题。
|
||||
|
||||
---
|
||||
|
||||
## 一、两种登录方式对比
|
||||
|
||||
| 方式 | 使用场景 | SDK | 核心API |
|
||||
|------|----------|-----|---------|
|
||||
| **飞书内免登** | 用户在飞书客户端内打开应用 | H5 JSSDK | `tt.requestAuthCode()` |
|
||||
| **飞书外扫码登录** | 用户在普通浏览器中打开 | QR SDK | `window.QRLogin()` |
|
||||
|
||||
---
|
||||
|
||||
## 二、飞书内免登(静默授权)
|
||||
|
||||
### 2.1 完整流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 用户在飞书客户端内打开应用 │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端检测环境 (navigator.userAgent 包含 Lark/feishu/lark) │
|
||||
│ 同时在 URL 中携带 company 参数 (如 ?company=2) │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 等待飞书 H5 JSSDK 加载完成 │
|
||||
│ - 检查 window.h5sdk?.ready │
|
||||
│ - 如果存在,调用 h5sdk.ready() 等待 tt 对象注入 │
|
||||
│ - 如果不存在,轮询等待 window.tt 出现(最多5秒) │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 获取 appId │
|
||||
│ 1. 优先从 URL 参数获取 (?app_id=xxx) │
|
||||
│ 2. 如果没有,从 URL 获取 company 参数 │
|
||||
│ 3. 调用 GET /api/v1/auth/feishu-config?company=2 │
|
||||
│ 4. 后端返回该公司的 appId │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 调用 tt.requestAuthCode({ │
|
||||
│ appId: "cli_xxx", │
|
||||
│ redirectUri: "https://xxx.com/login/callback" │
|
||||
│ }) │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ success 回调获取 auth_code (临时授权码,5分钟有效) │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端调用 POST /api/v1/auth/feishu-login │
|
||||
│ { auth_code: "xxx", company: "2" } │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 后端处理并返回 token 和 user │
|
||||
│ 前端保存到 localStorage 和 Zustand store │
|
||||
│ 跳转到 dashboard 或 redirect 参数指定的页面 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 关键代码
|
||||
|
||||
#### 引入 SDK (index.html)
|
||||
```html
|
||||
<!-- 飞书 H5 JSSDK(网页应用免登用) -->
|
||||
<script src="https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.19.js"></script>
|
||||
```
|
||||
|
||||
#### 类型声明
|
||||
```typescript
|
||||
declare global {
|
||||
interface Window {
|
||||
tt?: any
|
||||
h5sdk?: {
|
||||
ready: (callback: () => void) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 免登实现
|
||||
```typescript
|
||||
// 飞书内免登
|
||||
const attemptFeishuAutoLogin = async (companyParam?: string) => {
|
||||
// 如果 h5sdk 存在,等待 ready 后再调用
|
||||
if (window.h5sdk?.ready) {
|
||||
window.h5sdk.ready(() => {
|
||||
performFeishuLoginWithAppId(companyParam)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果 tt 对象已存在,直接使用
|
||||
if (window.tt) {
|
||||
performFeishuLoginWithAppId(companyParam)
|
||||
return
|
||||
}
|
||||
|
||||
// 轮询等待 tt 对象(最多 5 秒)
|
||||
let attempts = 0
|
||||
const maxAttempts = 50
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
attempts++
|
||||
if (window.tt) {
|
||||
clearInterval(checkInterval)
|
||||
performFeishuLoginWithAppId(companyParam)
|
||||
} else if (attempts >= maxAttempts) {
|
||||
clearInterval(checkInterval)
|
||||
console.error('等待 tt 对象超时')
|
||||
setLoading(false)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 获取 appId 并执行免登
|
||||
const performFeishuLoginWithAppId = async (companyParam?: string) => {
|
||||
let appId: string | undefined | null
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
appId = urlParams.get('app_id')
|
||||
|
||||
if (!appId && companyParam) {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/auth/feishu-config?company=${companyParam}`)
|
||||
const result = await response.json()
|
||||
if (result.code === 0 && result.data.app_id) {
|
||||
appId = result.data.app_id
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取 appId 失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
performFeishuLogin(companyParam, appId || undefined)
|
||||
}
|
||||
|
||||
// 执行飞书免登
|
||||
const performFeishuLogin = async (companyParam?: string, appId?: string | null) => {
|
||||
if (!window.tt) {
|
||||
setLoading(false)
|
||||
Toast.show('飞书环境异常,请刷新重试')
|
||||
return
|
||||
}
|
||||
|
||||
if (!appId) {
|
||||
setLoading(false)
|
||||
Toast.show('飞书应用配置错误')
|
||||
return
|
||||
}
|
||||
|
||||
const redirectUri = `${window.location.origin}/login/callback`
|
||||
|
||||
window.tt.requestAuthCode({
|
||||
appId: appId,
|
||||
redirectUri: redirectUri,
|
||||
success: (res: any) => {
|
||||
handleFeishuLogin(res.code, companyParam)
|
||||
},
|
||||
fail: (error: any) => {
|
||||
console.error('免登失败:', error)
|
||||
setLoading(false)
|
||||
Toast.show('免登失败:' + (error.errMsg || JSON.stringify(error)))
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Token 过期自动刷新
|
||||
|
||||
当 API 返回 401 时,在飞书环境中自动刷新 token,用户无感知:
|
||||
|
||||
```typescript
|
||||
// api.ts - 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response) => { ... },
|
||||
async (error) => {
|
||||
const originalRequest = error.config
|
||||
|
||||
// 处理 401 未授权错误
|
||||
if (error.response?.status === 401 || error.response?.data?.code === 401) {
|
||||
// 防止死循环:如果已经重试过一次,不再重试
|
||||
if (originalRequest._retry) {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(new Error('登录已过期'))
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试飞书免登刷新 token
|
||||
await handleUnauthorized()
|
||||
// token 刷新成功,重试原始请求
|
||||
originalRequest._retry = true
|
||||
originalRequest.headers.Authorization = `Bearer ${localStorage.getItem('token')}`
|
||||
return api.request(originalRequest)
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error('登录已过期'))
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 2.4 常见错误和解决方案
|
||||
|
||||
| 错误信息 | 原因 | 解决方案 |
|
||||
|---------|------|----------|
|
||||
| `requestAuthCode:fail invalid appId` | appId 参数为空 | 确保从 URL 或后端获取到正确的 appId |
|
||||
| `requestAuthCode:fail invalid url` | 缺少 redirectUri 参数 | 添加 `redirectUri: ${window.location.origin}/login/callback` |
|
||||
| `等待 tt 对象超时` | H5 JSSDK 未加载或未注入 tt | 确保引入了 H5 JSSDK,使用 h5sdk.ready() |
|
||||
| `tt.requestAccess is not a function` | API 名称错误 | 应使用 `tt.requestAuthCode` 不是 `requestAccess` |
|
||||
|
||||
---
|
||||
|
||||
## 三、飞书外扫码登录
|
||||
|
||||
### 3.1 完整流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 用户点击"飞书扫码登录"按钮 │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端调用 POST /api/v1/auth/qrcode-login │
|
||||
│ 后端创建 session,返回 auth_url 和 session_id │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端使用 QRLogin SDK 初始化二维码 │
|
||||
│ window.QRLogin({ id: "xxx", goto: authUrl }) │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 用户用飞书 APP 扫码 │
|
||||
│ QR SDK 触发 message 事件,返回 tmp_code │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端重定向到飞书授权页面 │
|
||||
│ window.location.href = authUrl + "&tmp_code=" + tmp_code │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 用户在手机上点击"允许" │
|
||||
│ 飞书重定向到 /login/callback?code=xxx&state=session_id │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 后端处理回调,更新 session 状态为 success │
|
||||
│ 返回包含 token 的 HTML 页面 │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ HTML 页面 JS 将 token 保存到 localStorage │
|
||||
│ 跳转到 dashboard │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 关键代码
|
||||
|
||||
#### 引入 SDK (index.html)
|
||||
```html
|
||||
<!-- 飞书扫码登录 SDK(飞书外扫码登录用) -->
|
||||
<script src="https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.3.js"></script>
|
||||
```
|
||||
|
||||
#### 二维码初始化
|
||||
```typescript
|
||||
const initQRCode = async () => {
|
||||
// 生成登录会话
|
||||
const response = await fetch('/api/v1/auth/qrcode-login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ company: companyParam }),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
const authUrl = result.data.auth_url
|
||||
|
||||
if (window.QRLogin) {
|
||||
qrObjRef.current = window.QRLogin({
|
||||
id: 'feishu-qrcode-container',
|
||||
goto: authUrl,
|
||||
width: '250',
|
||||
height: '250',
|
||||
style: 'width:300px;height:270px;border:none'
|
||||
})
|
||||
|
||||
// 监听扫码后的 message 事件
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (!qrObjRef.current?.matchOrigin(event.origin)) return
|
||||
if (!qrObjRef.current?.matchData(event.data)) return
|
||||
|
||||
const loginTmpCode = event.data?.tmp_code
|
||||
if (loginTmpCode) {
|
||||
// 重定向到飞书授权页面
|
||||
const authUrlWithCode = `${authUrl}&tmp_code=${loginTmpCode}`
|
||||
window.location.href = authUrlWithCode
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 常见错误和解决方案
|
||||
|
||||
| 错误信息 | 原因 | 解决方案 |
|
||||
|---------|------|----------|
|
||||
| `缺少 company、app_id 或 tenant_key 参数` | 请求时缺少必要参数 | 确保 URL 中有 `?company=2` 参数 |
|
||||
| 二维码不显示 | QR SDK 未加载或容器未渲染 | 确保 SDK 已加载,使用 setTimeout 等待 DOM 渲染 |
|
||||
| 扫码后没反应 | message 事件未正确监听 | 检查 matchOrigin 和 matchData 安全校验 |
|
||||
|
||||
---
|
||||
|
||||
## 四、后端接口
|
||||
|
||||
### 4.1 获取飞书配置
|
||||
```php
|
||||
/**
|
||||
* GET /api/v1/auth/feishu-config?company=2
|
||||
* 返回指定公司的飞书应用 appId
|
||||
*/
|
||||
public function feishuConfig(Request $request): Response
|
||||
{
|
||||
$companyParam = $request->get('company');
|
||||
$company = \app\model\Company::find($companyParam);
|
||||
|
||||
$feishuApp = \app\model\CompanyFeishuApp::where('company_id', $company->id)
|
||||
->where('system_id', 1)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
|
||||
return $this->success([
|
||||
'app_id' => $feishuApp->app_id,
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 飞书登录
|
||||
```php
|
||||
/**
|
||||
* POST /api/v1/auth/feishu-login
|
||||
* 用 auth_code 换取用户信息和 token
|
||||
*/
|
||||
public function feishuLogin(Request $request): Response
|
||||
{
|
||||
$authCode = $request->post('auth_code');
|
||||
$companyParam = $request->post('company');
|
||||
|
||||
// 用 auth_code 获取用户信息
|
||||
$feishuService = new FeishuService();
|
||||
$feishuService->setCompany($companyParam);
|
||||
$feishuData = $feishuService->getUserInfo($authCode);
|
||||
|
||||
// 创建或更新用户
|
||||
$user = $feishuService->createOrUpdateUser($feishuData, $companyParam);
|
||||
|
||||
// 生成 JWT token
|
||||
$token = JwtUtil::encode([...]);
|
||||
|
||||
return $this->success([
|
||||
'token' => $token,
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、调试技巧
|
||||
|
||||
### 5.1 环境检测页面
|
||||
创建 `/feishu-env-test` 页面,用于检测飞书环境:
|
||||
|
||||
```typescript
|
||||
// 检测项目:
|
||||
// 1. navigator.userAgent
|
||||
// 2. window.tt 是否存在
|
||||
// 3. window.h5sdk 是否存在
|
||||
// 4. tt.requestAuthCode 是否可用
|
||||
// 5. 实际调用 tt.requestAuthCode 测试
|
||||
```
|
||||
|
||||
### 5.2 常用 URL 参数
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `company` | 公司ID(必需) | `?company=2` |
|
||||
| `app_id` | 飞书应用ID(可选) | `?app_id=cli_xxx` |
|
||||
| `redirect` | 登录成功后跳转地址 | `?redirect=/dashboard` |
|
||||
| `auth_code` | 飞书授权码(回调时使用) | `?auth_code=xxx` |
|
||||
|
||||
### 5.3 日志关键点
|
||||
```typescript
|
||||
console.log('🔍 company 参数:', companyParam)
|
||||
console.log('🔍 appId:', appId)
|
||||
console.log('🔍 redirect_uri:', redirectUri)
|
||||
console.log('✅ 免登成功, code:', res.code)
|
||||
console.error('❌ 免登失败:', error)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、最佳实践
|
||||
|
||||
### 6.1 不要依赖本地 token 检查
|
||||
**问题**:前端检查 localStorage token 存在就直接跳转,可能导致使用过期 token。
|
||||
|
||||
**解决**:由后端验证 token 有效性,前端只负责在 401 时处理。
|
||||
|
||||
### 6.2 防止死循环
|
||||
**问题**:401 → 刷新 token → 重试请求 → 又 401 → 无限循环
|
||||
|
||||
**解决**:使用 `_retry` 标记,每个请求只重试一次。
|
||||
|
||||
```typescript
|
||||
if (originalRequest._retry) {
|
||||
// 已经重试过,不再重试
|
||||
return Promise.reject(new Error('登录已过期'))
|
||||
}
|
||||
originalRequest._retry = true
|
||||
```
|
||||
|
||||
### 6.3 参数传递的完整性
|
||||
**问题**:从其他页面跳转到登录页时,company 参数可能丢失。
|
||||
|
||||
**解决**:从 redirect 参数中提取 company:
|
||||
|
||||
```typescript
|
||||
let companyParam = urlParams.get('company')
|
||||
if (!companyParam) {
|
||||
const redirect = urlParams.get('redirect')
|
||||
if (redirect) {
|
||||
const redirectUrl = new URL(redirect, window.location.origin)
|
||||
companyParam = redirectUrl.searchParams.get('company')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 错误提示的准确性
|
||||
**问题**:显示误导性的错误信息(如"飞书应用配置错误"但实际已登录成功)。
|
||||
|
||||
**解决**:只在确定失败时显示 Toast,可能成功的情况用 console.error 记录。
|
||||
|
||||
---
|
||||
|
||||
## 七、飞书开放平台配置
|
||||
|
||||
### 7.1 应用类型
|
||||
- **网页应用** - 用于飞书内免登和飞书外扫码登录
|
||||
- 注意:飞书没有"H5应用"这个类型,只有"网页应用"
|
||||
|
||||
### 7.2 必配项
|
||||
| 配置项 | 说明 | 示例 |
|
||||
|--------|------|------|
|
||||
| 重定向 URL | 扫码登录回调地址 | `https://xxx.com/login/callback` |
|
||||
| 可信域名 | 允许嵌入应用的域名 | `xxx.com` |
|
||||
|
||||
### 7.3 权限配置
|
||||
- 获取用户基本信息
|
||||
- 获取用户邮箱
|
||||
- 获取用户手机号
|
||||
|
||||
---
|
||||
|
||||
## 八、相关文件路径
|
||||
|
||||
```
|
||||
/www/x-server-mobile/
|
||||
├── frontend/
|
||||
│ ├── index.html # SDK 引入
|
||||
│ ├── src/
|
||||
│ │ ├── pages/
|
||||
│ │ │ ├── login/
|
||||
│ │ │ │ ├── index.tsx # 登录页主逻辑
|
||||
│ │ │ │ └── QRCodeLogin.tsx # 扫码登录组件
|
||||
│ │ │ └── feishu-env-test.tsx # 环境检测页
|
||||
│ │ └── services/
|
||||
│ │ └── api.ts # API 封装(含 401 处理)
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── controller/
|
||||
│ │ │ └── AuthController.php # 认证控制器
|
||||
│ │ └── model/
|
||||
│ │ ├── Company.php # 公司模型
|
||||
│ │ └── CompanyFeishuApp.php # 飞书应用配置
|
||||
│ └── config/
|
||||
│ └── route.php # 路由配置
|
||||
└── docs/
|
||||
├── FEISHU_LOGIN_EXPERIENCE.md # 本文档
|
||||
└── feishu-qrcode-login.md # 原始文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、检查清单
|
||||
|
||||
部署前检查:
|
||||
|
||||
- [ ] 飞书开放平台已配置重定向 URL
|
||||
- [ ] index.html 已引入 H5 JSSDK 和 QR SDK
|
||||
- [ ] 后端 `/api/v1/auth/feishu-config` 接口正常
|
||||
- [ ] 后端 `/api/v1/auth/feishu-login` 接口正常
|
||||
- [ ] 后端 `/api/v1/auth/qrcode-login` 接口正常
|
||||
- [ ] 后端 `/login/callback` 路由已配置
|
||||
- [ ] Nginx 回调路由代理配置正确
|
||||
- [ ] 数据库 company 表有对应的飞书应用配置
|
||||
- [ ] 测试 URL 包含 company 参数(如 `?company=2`)
|
||||
|
||||
---
|
||||
|
||||
## 十、快速调试命令
|
||||
|
||||
```bash
|
||||
# 查看前端日志
|
||||
# 浏览器控制台
|
||||
|
||||
# 查看后端日志
|
||||
docker logs x-server-backend -f --tail 50
|
||||
|
||||
# 重启后端
|
||||
docker compose restart backend
|
||||
|
||||
# 构建前端
|
||||
cd /www/x-server-mobile/frontend
|
||||
npm run build
|
||||
|
||||
# 访问环境检测页
|
||||
https://xxx.com/feishu-env-test?company=2
|
||||
|
||||
# 访问登录页
|
||||
https://xxx.com/login?company=2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-02-28
|
||||
**维护者**: mzl
|
||||
@@ -0,0 +1,208 @@
|
||||
# 飞书通知模块使用文档
|
||||
|
||||
## 概述
|
||||
|
||||
`FeishuNotificationService` 是一个统一的飞书消息通知服务,支持多种卡片类型的消息通知,可供所有业务模块使用。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 多种卡片类型(审批通知、系统通知、提醒通知)
|
||||
- ✅ 批量发送支持
|
||||
- ✅ 多公司支持
|
||||
- ✅ 模板化配置
|
||||
- ✅ 失败重试机制
|
||||
- ✅ 详细日志记录
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 初始化服务
|
||||
|
||||
```php
|
||||
use app\service\FeishuNotificationService;
|
||||
|
||||
$notification = new FeishuNotificationService();
|
||||
$notification->setCompany($companyId);
|
||||
// 可选:指定应用
|
||||
$notification->setApp($appId);
|
||||
```
|
||||
|
||||
### 2. 发送审批通知
|
||||
|
||||
```php
|
||||
$notification->sendApprovalNotification([
|
||||
// 必填
|
||||
'user_id' => $approverId, // 接收人ID
|
||||
'title' => '新的审批申请', // 通知标题
|
||||
'applicant' => '张三', // 申请人姓名
|
||||
'type' => '出差申请', // 申请类型
|
||||
'content' => '前往上海参加客户会议', // 申请内容
|
||||
|
||||
// 可选
|
||||
'url' => 'https://xxx/approval/123', // 详情链接
|
||||
'approve_url' => 'https://xxx/approve/123', // 通过按钮链接
|
||||
'reject_url' => 'https://xxx/reject/123', // 拒绝按钮链接
|
||||
]);
|
||||
```
|
||||
|
||||
**卡片样式:**
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ 📋 新的审批申请 │
|
||||
├─────────────────────────────┤
|
||||
│ 申请人: 张三 申请类型: 出差 │
|
||||
│ │
|
||||
│ 申请内容: │
|
||||
│ 前往上海参加客户会议 │
|
||||
│ │
|
||||
│ [通过] [拒绝] [查看详情] │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. 发送系统通知
|
||||
|
||||
```php
|
||||
$notification->sendSystemNotification([
|
||||
// 必填
|
||||
'user_ids' => [1, 2, 3], // 接收人ID数组
|
||||
'title' => '系统通知', // 通知标题
|
||||
'content' => '系统将于今晚维护', // 通知内容
|
||||
|
||||
// 可选
|
||||
'url' => 'https://xxx/notice', // 跳转链接
|
||||
]);
|
||||
```
|
||||
|
||||
### 4. 发送提醒通知
|
||||
|
||||
```php
|
||||
$notification->sendReminderNotification([
|
||||
// 必填
|
||||
'user_id' => $userId, // 接收人ID
|
||||
'title' => '待办提醒', // 提醒标题
|
||||
'content' => '您有3条待审批', // 提醒内容
|
||||
'level' => 'warning', // 级别: info/warning/urgent
|
||||
|
||||
// 可选
|
||||
'url' => 'https://xxx/todo', // 跳转链接
|
||||
]);
|
||||
```
|
||||
|
||||
**提醒级别:**
|
||||
- `info` (💡 蓝色) - 普通提醒
|
||||
- `warning` (⚠️ 橙色) - 警告提醒
|
||||
- `urgent` (🚨 红色) - 紧急提醒
|
||||
|
||||
### 5. 批量发送
|
||||
|
||||
```php
|
||||
$notification->sendBatchNotification([
|
||||
'user_ids' => [1, 2, 3], // 接收人ID数组
|
||||
'type' => 'approval', // 通知类型: approval/system/reminder
|
||||
// ... 其他参数根据类型而定
|
||||
]);
|
||||
```
|
||||
|
||||
## 业务场景示例
|
||||
|
||||
### 出差申请审批通知
|
||||
|
||||
```php
|
||||
// 在 BusinessTripService 中
|
||||
$notification = new FeishuNotificationService();
|
||||
$notification->setCompany($companyId);
|
||||
|
||||
// 获取审批人列表
|
||||
$approvers = $this->getApprovers($tripId);
|
||||
|
||||
foreach ($approvers as $approver) {
|
||||
$notification->sendApprovalNotification([
|
||||
'user_id' => $approver->id,
|
||||
'title' => '新的出差申请',
|
||||
'applicant' => $trip->user->name,
|
||||
'type' => '出差申请',
|
||||
'content' => $trip->reason,
|
||||
'url' => config('app.url') . "/approval/trip/{$tripId}",
|
||||
'approve_url' => config('app.url') . "/api/approve/trip/{$tripId}/approve",
|
||||
'reject_url' => config('app.url') . "/api/approve/trip/{$tripId}/reject",
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### 审批结果通知
|
||||
|
||||
```php
|
||||
// 审批通过后通知申请人
|
||||
$notification->sendSystemNotification([
|
||||
'user_ids' => [$trip->user_id],
|
||||
'title' => '审批通过',
|
||||
'content' => "您的出差申请已通过\n\n目的地: {$trip->destination}\n时间: {$trip->start_date}",
|
||||
'url' => config('app.url') . "/business-trip/{$tripId}",
|
||||
]);
|
||||
```
|
||||
|
||||
### 待办提醒
|
||||
|
||||
```php
|
||||
// 每天提醒待审批事项
|
||||
$pendingApprovals = Approval::where('status', 'pending')->get();
|
||||
|
||||
foreach ($pendingApprovals as $approval) {
|
||||
$notification->sendReminderNotification([
|
||||
'user_id' => $approval->approver_id,
|
||||
'title' => '待办提醒',
|
||||
'content' => "您有 {$approval->count} 条待审批事项",
|
||||
'level' => 'warning',
|
||||
'url' => config('app.url') . "/approval",
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
- **单个发送**: 返回 `bool` - `true` 成功,`false` 失败
|
||||
- **批量发送**: 返回 `array` - key 为 user_id,value 为 bool
|
||||
|
||||
```php
|
||||
// 批量发送示例
|
||||
$results = $notification->sendSystemNotification([
|
||||
'user_ids' => [1, 2, 3],
|
||||
'title' => '系统通知',
|
||||
'content' => '系统维护通知',
|
||||
]);
|
||||
|
||||
// $results = [1 => true, 2 => false, 3 => true]
|
||||
$successCount = count(array_filter($results));
|
||||
$failCount = count($results) - $successCount;
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
服务会自动捕获异常并记录日志,不会中断业务流程:
|
||||
|
||||
```php
|
||||
// 即使发送失败,也不会抛出异常
|
||||
$result = $notification->sendApprovalNotification($params);
|
||||
if (!$result) {
|
||||
// 发送失败,日志已自动记录
|
||||
// 可在此处添加降级处理逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 配置文件
|
||||
|
||||
配置文件位于 `config/feishu_notification.php`,可自定义:
|
||||
- 卡片模板样式
|
||||
- 重试次数和间隔
|
||||
- 日志级别
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **用户必须绑定飞书**: 接收人必须已绑定飞书账号(`feishu_user_id` 不为空)
|
||||
2. **公司必须配置飞书应用**: 公司需要配置有效的飞书应用
|
||||
3. **链接必须可访问**: 卡片中的链接需要用户可以访问
|
||||
4. **内容长度限制**: 飞书对消息内容有长度限制,建议摘要控制在 200 字以内
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [飞书开放平台 - 消息卡片](https://open.feishu.cn/document/ukTMukTMukTM/uEjNwUjLxYDM14SM2ATN)
|
||||
- [飞书开放平台 - 发送消息](https://open.feishu.cn/document/ukTMukTMukTM/uUjNwUjL2YDM14SN2ATN)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,148 @@
|
||||
# 出差申请模块 (Business Trip Module)
|
||||
|
||||
## 概述
|
||||
|
||||
出差申请模块是审批流程的具体业务实现,支持出差行程管理、审批人选择、抄送人选择、草稿自动保存等功能。
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 行程管理 | 支持添加多个出差行程(城市、日期、住宿) |
|
||||
| 审批人选择 | 单选审批人,支持搜索 |
|
||||
| 抄送人选择 | 多选抄送人,支持搜索 |
|
||||
| 草稿保存 | 自动保存到 localStorage,刷新后恢复 |
|
||||
| 默认审批人 | 自动加载历史审批人 |
|
||||
|
||||
---
|
||||
|
||||
## API 接口
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/business-trip/list` | GET | 获取出差列表 |
|
||||
| `/api/v1/business-trip/detail/:id` | GET | 获取出差详情 |
|
||||
| `/api/v1/business-trip/create` | POST | 创建出差申请 |
|
||||
| `/api/v1/business-trip/update/:id` | POST | 更新出差申请 |
|
||||
| `/api/v1/business-trip/submit/:id` | POST | 提交审批 |
|
||||
| `/api/v1/business-trip/cancel/:id` | POST | 撤销申请 |
|
||||
| `/api/v1/business-trip/delete/:id` | DELETE | 删除草稿 |
|
||||
| `/api/v1/business-trip/default-approvers` | GET | 获取默认审批人 |
|
||||
|
||||
---
|
||||
|
||||
## 前端关键文件
|
||||
|
||||
```
|
||||
frontend/src/pages/business-trip/
|
||||
├── list.tsx # 出差列表页
|
||||
├── apply.tsx # 出差申请页
|
||||
├── detail.tsx # 出差详情页
|
||||
├── select-approver.tsx # 选择审批人
|
||||
└── select-cc.tsx # 选择抄送人
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 草稿自动保存
|
||||
|
||||
### 保存时机
|
||||
- 表单字段变化时
|
||||
- 添加/删除行程时
|
||||
- 选择审批人/抄送人时
|
||||
|
||||
### 保存内容
|
||||
```typescript
|
||||
{
|
||||
trips: [...], // 出差行程
|
||||
approvers: [...], // 审批人列表
|
||||
ccUsers: [...], // 抄送人列表
|
||||
reason: string, // 出差事由
|
||||
savedAt: timestamp // 保存时间
|
||||
}
|
||||
```
|
||||
|
||||
### 恢复逻辑
|
||||
- 进入申请页时检查 localStorage
|
||||
- 如果存在草稿且未超过24小时,提示恢复
|
||||
- 用户可选择恢复或放弃
|
||||
|
||||
---
|
||||
|
||||
## 表单验证
|
||||
|
||||
### 必填字段
|
||||
- 出差行程(至少一条)
|
||||
- 每条行程:出发城市、目的城市、出发日期、返回日期
|
||||
- 审批人(至少一人)
|
||||
- 出差事由
|
||||
|
||||
### 日期验证
|
||||
- 出发日期不能早于今天
|
||||
- 返回日期不能早于出发日期
|
||||
- 多条行程日期不能重叠
|
||||
|
||||
---
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### business_trip (出差申请)
|
||||
```sql
|
||||
id -- 主键
|
||||
company_id -- 公司ID
|
||||
applicant_id -- 申请人ID
|
||||
title -- 标题
|
||||
reason -- 出差事由
|
||||
status -- 状态 (draft, pending, approved, rejected, cancelled)
|
||||
current_node_id -- 当前节点ID
|
||||
created_at
|
||||
updated_at
|
||||
```
|
||||
|
||||
### business_trip_trip (出差行程)
|
||||
```sql
|
||||
id -- 主键
|
||||
business_trip_id -- 出差申请ID
|
||||
from_city -- 出发城市
|
||||
to_city -- 目的城市
|
||||
start_date -- 出发日期
|
||||
end_date -- 返回日期
|
||||
hotel_required -- 是否需要住宿
|
||||
hotel_name -- 酒店名称(可选)
|
||||
```
|
||||
|
||||
### business_trip_approver (审批人)
|
||||
```sql
|
||||
id -- 主键
|
||||
business_trip_id -- 出差申请ID
|
||||
approver_id -- 审批人ID
|
||||
is_default -- 是否默认审批人
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 状态流转
|
||||
|
||||
```
|
||||
draft (草稿)
|
||||
│ 提交
|
||||
▼
|
||||
pending (待审批)
|
||||
│ 通过 │ 拒绝 │ 撤销
|
||||
▼ ▼ ▼
|
||||
approved rejected cancelled
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [审批流程模块](../approval/README.md)
|
||||
- [PROJECT_ISSUES.md](../../PROJECT_ISSUES.md)
|
||||
|
||||
---
|
||||
|
||||
**负责人**: 待分配
|
||||
**最后更新**: 2026-02-28
|
||||
@@ -0,0 +1,183 @@
|
||||
# 用户管理模块 (User Module)
|
||||
|
||||
## 概述
|
||||
|
||||
用户管理模块负责用户信息、多租户公司管理、权限控制等功能。
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
|
||||
| 功能 | 说明 | 状态 |
|
||||
|------|------|------|
|
||||
| 用户信息 | 获取、更新用户信息 | ✅ |
|
||||
| 多租户支持 | 用户可关联多个公司 | ✅ |
|
||||
| 公司切换 | 切换当前公司 | ⏳ 待实现 |
|
||||
| 权限控制 | 基于角色的权限管理 | ⏳ 待实现 |
|
||||
| 密码修改 | 修改登录密码 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## API 接口
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/user/info` | GET | 获取用户信息 |
|
||||
| `/api/v1/user/menu` | GET | 获取用户菜单 |
|
||||
| `/api/v1/user/companies` | GET | 获取用户公司列表 |
|
||||
| `/api/v1/user/switch-company` | POST | 切换公司 |
|
||||
| `/api/v1/user/change-password` | POST | 修改密码 |
|
||||
| `/api/v1/user/list` | GET | 获取用户列表(管理员) |
|
||||
|
||||
---
|
||||
|
||||
## 前端关键文件
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── pages/
|
||||
│ ├── profile/ # 个人中心
|
||||
│ └── change-password/ # 修改密码
|
||||
├── stores/
|
||||
│ └── user.ts # 用户状态管理
|
||||
└── components/
|
||||
└── CompanySelector/ # 公司选择器(待实现)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端关键文件
|
||||
|
||||
```
|
||||
backend/app/
|
||||
├── Controller/
|
||||
│ └── UserController.php # 用户控制器
|
||||
├── Model/
|
||||
│ ├── User.php # 用户模型
|
||||
│ └── Company.php # 公司模型
|
||||
└── Middleware/
|
||||
└── AuthMiddleware.php # 认证中间件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 多租户设计
|
||||
|
||||
### 用户-公司关系
|
||||
|
||||
```
|
||||
user (用户)
|
||||
├── user_company (用户公司关联)
|
||||
│ ├── company_id
|
||||
│ ├── is_default -- 是否默认公司
|
||||
│ └── role -- 在该公司的角色
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 切换公司流程
|
||||
|
||||
```
|
||||
1. 用户选择公司
|
||||
↓
|
||||
2. 前端调用 /api/v1/user/switch-company
|
||||
↓
|
||||
3. 后端验证用户权限
|
||||
↓
|
||||
4. 更新 token 中的 company_id
|
||||
↓
|
||||
5. 返回新的 token
|
||||
↓
|
||||
6. 前端更新本地存储和状态
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 用户状态管理 (Zustand)
|
||||
|
||||
```typescript
|
||||
interface UserStore {
|
||||
user: User | null
|
||||
token: string | null
|
||||
login: (user: User, token: string) => void
|
||||
logout: () => void
|
||||
updateUser: (user: User) => void
|
||||
}
|
||||
|
||||
// 使用
|
||||
const { user, login, logout } = useUserStore()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Token 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": 1,
|
||||
"company_id": 2,
|
||||
"feishu_user_id": "ou_xxx",
|
||||
"union_id": "on_xxx",
|
||||
"exp": 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### user (用户)
|
||||
```sql
|
||||
id -- 主键
|
||||
username -- 用户名(登录用)
|
||||
email -- 邮箱
|
||||
phone -- 手机号
|
||||
name -- 姓名
|
||||
avatar -- 头像
|
||||
password -- 密码(加密)
|
||||
feishu_user_id -- 飞书用户ID
|
||||
feishu_union_id -- 飞书union_id
|
||||
created_at
|
||||
updated_at
|
||||
```
|
||||
|
||||
### company (公司)
|
||||
```sql
|
||||
id -- 主键
|
||||
name -- 公司名称
|
||||
logo -- 公司logo
|
||||
status -- 状态
|
||||
```
|
||||
|
||||
### user_company (用户公司关联)
|
||||
```sql
|
||||
id -- 主键
|
||||
user_id -- 用户ID
|
||||
company_id -- 公司ID
|
||||
role -- 角色
|
||||
is_default -- 是否默认公司
|
||||
created_at
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 权限设计(待实现)
|
||||
|
||||
### 角色类型
|
||||
|
||||
| 角色 | 代码 | 说明 |
|
||||
|------|------|------|
|
||||
| 超级管理员 | `super_admin` | 全部权限 |
|
||||
| 公司管理员 | `company_admin` | 公司内全部权限 |
|
||||
| 普通用户 | `user` | 基础权限 |
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [认证模块](../auth/README.md)
|
||||
- [数据库分析](../../database-analysis.md)
|
||||
|
||||
---
|
||||
|
||||
**负责人**: 待分配
|
||||
**最后更新**: 2026-02-28
|
||||
Reference in New Issue
Block a user