# 许可管理单位权限优化PRD

## 1. 需求背景

### 1.1 当前问题
- 现有权限体系：市局账号 vs 区局子管理员（通过grade区分）
- 现状：市局账号查看全部许可，区局子管理员仅能查看本区许可
- 局限：
  - 缺乏市级单位（局、委、办）和区级单位的层级管理
  - 上传许可文件时无法指定绑定的具体单位
  - 浏览方式低效（仅按行政区筛选）

### 1.2 优化目标
1. **单位层级管理**：引入市级单位和区级单位的概念
2. **权限细分**：
   - 市局账号：保留全部可见权限
   - 市级单位：可见自身 + 下属区级单位
   - 区局子管理员：可见下属所有单位
   - 区级单位：仅可见自身
3. **文件绑定优化**：上传许可可绑定到具体单位
4. **浏览效率提升**：从单一"选择区域"改为多维度"过滤器筛选"

### 1.3 保留内容
- ✅ 一市五区的行政区划不变
- ✅ 现有账户保留（作为市局和区局子管理员）
- ✅ 现有API保持兼容

## 2. 功能需求

### 2.1 单位管理功能

#### 2.1.1 单位级别定义
| 单位类型 | 说明 | 示例 | 可视范围 |
|---------|------|------|----------|
| admin | 市局管理员 | FSSJSJ（市级管理员） | 全部许可 |
| municipal | 市级单位 | 市监局、卫健局、交通局等 | 自身 + 下属区级单位 |
| district | 区局子管理员 | 禅城区、南海区等管理员 | 下属所有单位 |
| unit | 区级单位 | 禅城区市场监督管理局等 | 仅自身 |

#### 2.1.2 单位创建流程
1. **创建市级单位**
   - 指定单位名称、编码
   - 设置单位级别为 `municipal`
   - 可绑定多个区级行政区
   - 自动生成管理员账号

2. **创建区级单位**
   - 指定单位名称、编码
   - 设置单位级别为 `unit`
   - 绑定到具体行政区（禅城区、南海区等）
   - 关联到市级单位作为上级

### 2.2 权限控制功能

#### 2.2.1 可见性规则

```python
# 权限判断逻辑
def get_visible_permits(user, filters):
    dept = user.service_department
    unit_level = dept.unit_level

    if user.role == 'admin' or user.grade >= 90:
        # 市局：全部许可
        return query_all_permits(filters)

    elif unit_level == 'municipal':
        # 市级单位：自身 + 下属区级单位
        subordinate_ids = get_subordinate_departments(dept.id, level='unit')
        return query_permits_by_departments([dept.id] + subordinate_ids, filters)

    elif unit_level == 'district':
        # 区局子管理员：下属所有单位
        return query_permits_by_region(dept.region_id, filters)

    elif unit_level == 'unit':
        # 区级单位：仅自身
        return query_permits_by_departments([dept.id], filters)

    else:
        # 未知级别：无权限
        return []
```

#### 2.2.2 权限矩阵

| 用户类型 | 自身许可 | 下属单位许可 | 其他区许可 | 其他市级单位许可 |
|---------|---------|-------------|----------|----------------|
| 市局管理员 | ✅ | ✅ | ✅ | ✅ |
| 市级单位用户 | ✅ | ✅ | ❌ | ❌ |
| 区局子管理员 | ✅ | ✅ | ❌ | ❌ |
| 区级单位用户 | ✅ | ❌ | ❌ | ❌ |

### 2.3 文件上传绑定功能

#### 2.3.1 绑定规则

| 上传者类型 | 默认绑定 | 可修改性 | 可见用户 |
|----------|---------|---------|----------|
| 市局管理员 | 市局自身 | 可选择任意单位 | 所有用户 |
| 市级单位用户 | 市级单位自身 | 可选择下属区级单位 | 市局 + 相关单位 |
| 区局子管理员 | 所属区 | 可修改 | 市局 + 相关区局 |
| 区级单位用户 | 自身单位 | 不可修改 | 市局 + 上级区局 |

#### 2.3.2 绑定流程

```python
def handle_permit_upload(file_data, uploader_user, bound_department_id=None):
    # 1. 确定绑定单位
    if not bound_department_id:
        bound_department_id = get_default_binding(uploader_user)

    # 2. 保存文件
    file_id = save_permit_file(file_data)

    # 3. 记录绑定关系
    bind_permit_to_department(
        file_id=file_id,
        bound_department_id=bound_department_id,
        uploader_department_id=uploader_user.service_department_id,
        uploader_user_id=uploader_user.id
    )

    # 4. 返回可见用户列表
    return get_visible_users(bound_department_id, uploader_user)
```

### 2.4 筛选浏览功能

#### 2.4.1 筛选维度
1. **市级单位**：下拉选择（全部/具体单位）
2. **区级单位**：下拉选择（全部/具体单位）
3. **行政区**：下拉选择（全部/禅城区/南海区/.../市级）
4. **许可名称**：文本搜索

#### 2.4.2 交互方式
- 市级单位和区级单位联动选择
- 支持搜索过滤
- 重置按钮清空所有筛选
- 显示已筛选结果数量

## 3. 技术方案

### 3.1 数据库设计

#### 3.1.1 现有表调整

**service_departments 表新增字段**
```sql
ALTER TABLE service_departments
ADD COLUMN unit_level VARCHAR(20) DEFAULT 'unit' CHECK (unit_level IN ('admin', 'municipal', 'district', 'unit')),
ADD COLUMN allowed_regions TEXT;  -- 市级单位可访问的行政区，多个用逗号分隔
```

**permit_sources 表新增字段**
```sql
ALTER TABLE permit_sources
ADD COLUMN uploader_department_id UUID REFERENCES service_departments(id),
ADD COLUMN bound_department_id UUID REFERENCES service_departments(id);
```

#### 3.1.2 新建索引
```sql
CREATE INDEX idx_service_dept_unit_level ON service_departments(unit_level);
CREATE INDEX idx_service_dept_parent_level ON service_departments(parent_id, unit_level);
CREATE INDEX idx_permit_sources_bound_dept ON permit_sources(bound_department_id);
CREATE INDEX idx_permit_sources_uploader ON permit_sources(uploader_department_id);
```

#### 3.1.3 数据迁移脚本
```sql
-- 迁移现有数据
UPDATE service_departments SET unit_level = 'admin' WHERE grade >= 90;
UPDATE service_departments SET unit_level = 'district' WHERE parent_id IS NULL AND grade < 90;

-- 为现有表添加约束
ALTER TABLE service_departments ALTER COLUMN unit_level SET NOT NULL;
```

### 3.2 后端API设计

#### 3.2.1 新增API

**获取单位列表（支持层级）**
```
GET /admin/departments/tree
Response: {
  "success": true,
  "data": {
    "municipal_units": [
      {
        "id": "uuid-1",
        "name": "市监局",
        "code": "AMR_FS",
        "units": [
          {"id": "uuid-2", "name": "禅城区市监局", "region": "禅城区"},
          {"id": "uuid-3", "name": "南海区市监局", "region": "南海区"}
        ]
      }
    ]
  }
}
```

**筛选查询许可**
```
POST /admin/permits/filter
Request: {
  "municipal_dept_id": "uuid-1",
  "district_dept_id": "uuid-2",
  "region": "禅城区",
  "search_text": "电影院",
  "page": 1,
  "page_size": 20
}
Response: {
  "success": true,
  "data": {
    "permits": [...],
    "pagination": {
      "total": 100,
      "page": 1,
      "page_size": 20
    }
  }
}
```

**上传许可文件（新增绑定参数）**
```
POST /admin/permit-import/upload
Request: form-data
  - file: [Excel文件]
  - bound_department_id: "uuid-2"  # 可选，不传则自动绑定
Response: {
  "success": true,
  "data": {
    "session_id": "uuid",
    "bound_to": {
      "department_id": "uuid-2",
      "department_name": "禅城区市监局",
      "visible_to": ["admin_users", "related_users"]
    }
  }
}
```

#### 3.2.2 修改现有API

**获取许可列表（向后兼容）**
```
GET /getPermits?region=禅城区
```
- 保持现有参数不变
- 新增根据单位级别过滤的逻辑

### 3.3 前端设计

#### 3.3.1 筛选器UI

```html
<div class="filter-panel">
    <div class="filter-row">
        <div class="filter-item">
            <label>市级单位：</label>
            <select id="municipalDept">
                <option value="">全部</option>
                <option value="dept_1">市监局</option>
                <option value="dept_2">卫健局</option>
                <option value="dept_3">交通局</option>
            </select>
        </div>

        <div class="filter-item">
            <label>区级单位：</label>
            <select id="districtDept" disabled>
                <option value="">请先选择市级单位</option>
            </select>
        </div>

        <div class="filter-item">
            <label>行政区：</label>
            <select id="region">
                <option value="">全部</option>
                <option value="禅城区">禅城区</option>
                <option value="南海区">南海区</option>
                <option value="顺德区">顺德区</option>
                <option value="三水区">三水区</option>
                <option value="高明区">高明区</option>
                <option value="市级">市级</option>
            </select>
        </div>
    </div>

    <div class="filter-row">
        <div class="filter-item search-item">
            <label>搜索许可：</label>
            <input type="text" id="searchText" placeholder="输入许可名称...">
            <button class="btn btn-primary" onclick="applyFilters()">筛选</button>
            <button class="btn btn-secondary" onclick="resetFilters()">重置</button>
        </div>
    </div>

    <div class="filter-result-info">
        <span id="resultCount">共找到 0 条许可</span>
    </div>
</div>
```

#### 3.3.2 联动逻辑

```javascript
// 市级单位选择变化时，动态加载区级单位
document.getElementById('municipalDept').addEventListener('change', function() {
    const municipalId = this.value;
    const districtSelect = document.getElementById('districtDept');

    if (!municipalId) {
        districtSelect.innerHTML = '<option value="">请先选择市级单位</option>';
        districtSelect.disabled = true;
        return;
    }

    // 加载该市级单位下的区级单位
    fetch(`/api/admin/departments/children?parent_id=${municipalId}`)
        .then(res => res.json())
        .then(data => {
            districtSelect.innerHTML = '<option value="">全部</option>';
            data.units.forEach(unit => {
                districtSelect.innerHTML += `<option value="${unit.id}">${unit.name}</option>`;
            });
            districtSelect.disabled = false;
        });
});

// 应用筛选
function applyFilters() {
    const filters = {
        municipal_dept_id: document.getElementById('municipalDept').value,
        district_dept_id: document.getElementById('districtDept').value,
        region: document.getElementById('region').value,
        search_text: document.getElementById('searchText').value
    };

    // 调用API获取数据
    fetch('/api/admin/permits/filter', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(filters)
    }).then(res => res.json())
      .then(data => {
          renderPermits(data.permits);
          updateResultCount(data.pagination.total);
      });
}
```

#### 3.3.3 许可列表展示

```html
<div class="permit-list">
    <div class="permit-item" v-for="permit in permits">
        <div class="permit-header">
            <h3>{{ permit.name }}</h3>
            <span class="permit-dept">{{ permit.department_name }}</span>
            <span class="permit-region">{{ permit.region_name }}</span>
        </div>
        <div class="permit-meta">
            <span class="permit-theme">{{ permit.theme_name }}</span>
            <span class="permit-risk-count">风险 {{ permit.risk_count }} 条</span>
        </div>
        <div class="permit-actions">
            <button @click="viewPermit(permit.id)">查看详情</button>
            <button @click="downloadFile(permit.file_id)" v-if="permit.file_id">下载文件</button>
        </div>
    </div>
</div>
```

## 4. 实施计划

### 4.1 阶段一：后端开发（预计3-5天）

#### 4.1.1 数据库迁移
- [ ] 编写并执行ALTER TABLE语句
- [ ] 创建索引
- [ ] 执行数据迁移脚本
- [ ] 验证迁移结果

**负责人**：后端开发
**交付物**：
- 迁移脚本文件
- 数据迁移报告

#### 4.1.2 权限逻辑调整
- [ ] 修改 `licensing_repo.py` 中的 `list_permits_for_region` 函数
- [ ] 新增 `get_subordinate_departments` 函数
- [ ] 新增 `get_visible_permits` 统一接口
- [ ] 编写单元测试

**负责人**：后端开发
**交付物**：
- 权限控制代码
- 单元测试用例
- API文档

#### 4.1.3 新增API开发
- [ ] `/admin/departments/tree` - 获取单位树
- [ ] `/admin/departments/children` - 获取子单位
- [ ] `/admin/permits/filter` - 筛选查询
- [ ] 修改 `/admin/permit-import/upload` 支持绑定单位

**负责人**：后端开发
**交付物**：
- API接口代码
- API文档
- Postman测试集合

#### 4.1.4 现有API兼容性测试
- [ ] 测试 `/getPermits` 接口
- [ ] 测试所有 `/admin/*` 接口
- [ ] 确保向后兼容

**负责人**：后端开发 + 测试
**交付物**：
- 测试报告
- 性能测试结果

### 4.2 阶段二：前端开发（预计3-4天）

#### 4.2.1 筛选器组件开发
- [ ] 设计筛选器UI样式
- [ ] 实现下拉联动逻辑
- [ ] 实现搜索和重置功能
- [ ] 响应式适配

**负责人**：前端开发
**交付物**：
- HTML/CSS/JS代码
- 组件文档

#### 4.2.2 API集成
- [ ] 集成新的筛选API
- [ ] 实现数据加载和分页
- [ ] 优化加载性能
- [ ] 错误处理

**负责人**：前端开发
**交付物**：
- 集成了新API的前端页面
- 前端测试报告

#### 4.2.3 文件上传绑定UI
- [ ] 添加绑定单位选择器
- [ ] 默认绑定逻辑提示
- [ ] 可见用户列表展示

**负责人**：前端开发
**交付物**：
- 文件上传界面
- 用户体验测试报告

#### 4.2.4 整体测试
- [ ] 功能测试
- [ ] 兼容性测试
- [ ] 性能测试
- [ ] 修复BUG

**负责人**：前端开发 + 测试
**交付物**：
- 测试报告
- 修复清单

### 4.3 阶段三：联调测试（预计2-3天）

#### 4.3.1 单元测试
- [ ] 后端API测试
- [ ] 前端功能测试
- [ ] 数据库操作测试

#### 4.3.2 集成测试
- [ ] 端到端流程测试
- [ ] 权限验证测试
- [ ] 文件上传下载测试

#### 4.3.3 性能测试
- [ ] 并发查询测试
- [ ] 大量数据查询测试
- [ ] 优化性能瓶颈

#### 4.3.4 用户验收测试
- [ ] 业务场景测试
- [ ] 界面易用性测试
- [ ] 需求确认

**负责人**：全体开发 + 产品 + 测试
**交付物**：
- 集成测试报告
- 性能测试报告
- UAT测试报告

### 4.4 阶段四：上线部署（预计1天）

#### 4.4.1 生产环境部署
- [ ] 数据库迁移
- [ ] 后端服务部署
- [ ] 前端页面部署
- [ ] 配置检查

#### 4.4.2 上线验证
- [ ] 功能验证
- [ ] 性能验证
- [ ] 监控检查

**负责人**：运维 + 开发
**交付物**：
- 上线检查清单
- 回滚预案

## 5. 风险评估

### 5.1 技术风险

| 风险项 | 影响程度 | 应对措施 |
|--------|---------|---------|
| 数据库迁移失败 | 高 | 提前备份，准备回滚脚本 |
| 权限逻辑复杂导致BUG | 中 | 详细单元测试，逐步发布 |
| 查询性能下降 | 中 | 添加索引，必要时优化SQL |
| API变更影响现有功能 | 中 | 保持向后兼容，新老API并行 |

### 5.2 业务风险

| 风险项 | 影响程度 | 应对措施 |
|--------|---------|---------|
| 权限设置错误导致数据泄露 | 高 | 严格测试，灰度发布 |
| 用户操作复杂，学习成本高 | 低 | 提供培训，更新文档 |
| 现有工作流程被破坏 | 中 | 保持核心功能不变，新增功能可选 |

### 5.3 应对预案

1. **数据安全**：每次部署前创建checkpoint备份
2. **功能回滚**：保留旧版本API至少3个月
3. **灰度发布**：新功能先开放给测试用户
4. **应急响应**：建立问题反馈群，24小时内响应

## 6. 成功标准

### 6.1 功能标准
- [ ] 单位层级管理功能完整可用
- [ ] 权限控制准确无误
- [ ] 文件上传绑定符合预期
- [ ] 筛选浏览高效便捷

### 6.2 性能标准
- [ ] API响应时间 < 1秒
- [ ] 页面加载时间 < 3秒
- [ ] 支持同时在线用户数 > 100
- [ ] 单位数量支持 > 100

### 6.3 用户体验标准
- [ ] 操作步骤减少 30%
- [ ] 筛选精度提升 50%
- [ ] 学习成本 < 30分钟
- [ ] 用户满意度 > 85%

## 7. 项目里程碑

| 里程碑 | 预计完成时间 | 交付物 |
|--------|-------------|--------|
| 需求评审通过 | T+0 | 本PRD文档 |
| 后端开发完成 | T+5 | 后端代码 + 测试 |
| 前端开发完成 | T+9 | 前端代码 + 测试 |
| 集成测试通过 | T+12 | 测试报告 |
| 生产环境上线 | T+13 | 上线系统 |

---

**编写人员**：[产品经理] + [技术负责人]
**评审人员**：[项目经理] + [开发团队] + [测试团队]
**最后更新**：2025-11-19
