|
|
@@ -0,0 +1,500 @@
|
|
|
+# Health Report Reallocation 实施计划
|
|
|
+
|
|
|
+> **面向 AI 代理的工作者**:必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
+
|
|
|
+> **目标**:新增 `POST /api/health/report/reassign` 接口,支持管理员/规划师重新分配报告归属,并触发五维能量/食材推荐维度重算;同时修复 `getUnlinkedReports` 查询 bug(familyId → family_id)。
|
|
|
+
|
|
|
+> **架构**:在现有 HealthReportService/HealthReportController 之上增量添加,复用已有的 `bindReportToMember` + 维度重算逻辑,无新建表/字段。
|
|
|
+
|
|
|
+> **技术栈**:Spring Boot 2.7.18 + MyBatis-Plus + JdbcTemplate + antd Vue 组件库
|
|
|
+
|
|
|
+> **已完成**:设计文档保存至 `specs/2026-10-05-health-report-reassign-design.md`
|
|
|
+
|
|
|
+> **计划保存位置**:`docs/superpowers/plans/2026-10-05-health-report-reassign-plan.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 1:新增 `POST /api/health/report/reassign` 控制器
|
|
|
+
|
|
|
+**文件**:`/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java`
|
|
|
+
|
|
|
+**位置**:在 `report/unlinked` 方法(约 1375 行)之后、`report/bind` 方法(约 1359 行)之前新增
|
|
|
+
|
|
|
+**代码块**:
|
|
|
+
|
|
|
+```java
|
|
|
+/**
|
|
|
+ * 重新分配报告归属
|
|
|
+ *
|
|
|
+ * <p>将报告从旧归属切换到新归属,或解除归属(subjectId 设为 null)。
|
|
|
+ * 归属变更后自动重新计算五维能量维度和食材推荐指数。</p>
|
|
|
+ *
|
|
|
+ * <p>权限检查:仅家庭成员或管理员可操作,通过 familyId 验证报告归属。</p>
|
|
|
+ *
|
|
|
+ * @param params 请求体:reportId (必填), subjectId (可为 null 表示解绑)
|
|
|
+ * @param familyId 当前登录用户的家庭ID(由 JwtInterceptor 注入)
|
|
|
+ * @return 操作结果
|
|
|
+ */
|
|
|
+@PostMapping("/report/reassign")
|
|
|
+public Result<String> reassignReport(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute(value = "familyId", required = false) Long familyId) {
|
|
|
+ Long reportId = ParamUtils.getLong(params.get("reportId"));
|
|
|
+ Long newSubjectId = ParamUtils.getLong(params.get("subjectId")); // null = unbind
|
|
|
+
|
|
|
+ healthReportService.reassignReport(reportId, newSubjectId, familyId);
|
|
|
+ return Result.success("归属已更新");
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**验证命令**:
|
|
|
+```bash
|
|
|
+mvn clean compile # 必须通过编译
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 2:实现 service 层 reassignReport 方法
|
|
|
+
|
|
|
+**文件**:`/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java`
|
|
|
+
|
|
|
+**位置**:在 `bindReportToMember` 方法(约 1185 行)之后、`getUnlinkedReports` 方法(约 1196 行)之前新增
|
|
|
+
|
|
|
+**代码块**:
|
|
|
+
|
|
|
+```java
|
|
|
+/**
|
|
|
+ * 重新分配报告归属
|
|
|
+ *
|
|
|
+ * <p>将报告从旧归属切换到新归属,或解除归属(subjectId 设为 null)。
|
|
|
+ * 归属变更后自动重新计算五维能量维度和食材推荐指数。</p>
|
|
|
+ *
|
|
|
+ * <p>流程:</p>
|
|
|
+ * <ol>
|
|
|
+ * <li>权限检查:确保报告属于当前家庭</li>
|
|
|
+ * <li>记录旧归属 subjectId</li>
|
|
|
+ * <li>更新 health_reports.subject_id</li>
|
|
|
+ * <li>若旧归属不为空:refreshFromGutReport + recalculateForUser</li>
|
|
|
+ * <li>若新归属不为空:refreshFromGutReport + recalculateForUser</li>
|
|
|
+ * <li>记录操作日志</li>
|
|
|
+ * </ol>
|
|
|
+ *
|
|
|
+ * @param reportId 报告ID
|
|
|
+ * @param newSubjectId 新的归属成员ID(null 表示解除绑定)
|
|
|
+ * @param familyId 家庭ID,用于权限验证
|
|
|
+ * @throws RuntimeException 当报告不存在或不属于该家庭时抛出
|
|
|
+ */
|
|
|
+@Transactional
|
|
|
+public void reassignReport(Long reportId, Long newSubjectId, Long familyId) {
|
|
|
+ HealthReport report = healthReportMapper.selectById(reportId);
|
|
|
+ if (report == null) {
|
|
|
+ throw new RuntimeException("报告不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 权限检查:确保报告属于该家庭
|
|
|
+ if (!familyId.equals(report.getFamilyId())) {
|
|
|
+ throw new RuntimeException("报告不属于该家庭");
|
|
|
+ }
|
|
|
+
|
|
|
+ Long oldSubjectId = report.getSubjectId();
|
|
|
+
|
|
|
+ // 无实际变化则直接返回
|
|
|
+ if (oldSubjectId != null && newSubjectId != null && oldSubjectId.equals(newSubjectId)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 更新 subject_id
|
|
|
+ String updateSql = "UPDATE health_reports SET subject_id = ? WHERE id = ?";
|
|
|
+ jdbcTemplate.update(updateSql, newSubjectId, reportId);
|
|
|
+ log.info("报告 {} 归属已更新:oldSubjectId={}, newSubjectId={}", reportId, oldSubjectId, newSubjectId);
|
|
|
+
|
|
|
+ // 2. 重算旧成员维度(如果原来有绑定)
|
|
|
+ if (oldSubjectId != null) {
|
|
|
+ dimensionScoreService.refreshFromGutReport(oldSubjectId, reportId);
|
|
|
+ foodRecommendService.recalculateForUser(oldSubjectId);
|
|
|
+ log.info("已重算旧归属成员 {} 的五维能量与食材推荐", oldSubjectId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 重算新成员维度(如果有新归属)
|
|
|
+ if (newSubjectId != null) {
|
|
|
+ dimensionScoreService.refreshFromGutReport(newSubjectId, reportId);
|
|
|
+ foodRecommendService.recalculateForUser(newSubjectId);
|
|
|
+ log.info("已重算新归属成员 {} 的五维能量与食材推荐", newSubjectId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 日志记录(可选:记录操作者)
|
|
|
+ String operator = String.valueOf(ThreadLocalUtils.getCurrentUserId());
|
|
|
+ log.info("报告归属变更: reportId={}, oldSubjectId={}, newSubjectId={}, operator={}",
|
|
|
+ reportId, oldSubjectId, newSubjectId, operator);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**验证命令**:
|
|
|
+```bash
|
|
|
+mvn clean compile # 必须通过编译
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 3:修复 `getUnlinkedReports` 查询 bug
|
|
|
+
|
|
|
+**文件**:`/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java`
|
|
|
+
|
|
|
+**位置**:将 `getUnlinkedReports(Long userId)` 方法(约 1196 行)重构为接受 `familyId`
|
|
|
+
|
|
|
+**代码块**:
|
|
|
+
|
|
|
+```java
|
|
|
+/**
|
|
|
+ * 获取家庭内所有未绑定归属的报告
|
|
|
+ *
|
|
|
+ * <p>查询条件:family_id = ? 且 subject_id IS NULL</p>
|
|
|
+ *
|
|
|
+ * @param familyId 家庭ID
|
|
|
+ * @return 未绑定归属的报告列表
|
|
|
+ */
|
|
|
+public List<HealthReport> getUnlinkedReports(Long familyId) {
|
|
|
+ return healthReportMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<HealthReport>()
|
|
|
+ .eq(HealthReport::getFamilyId, familyId) // ← 修复:改为 familyId
|
|
|
+ .isNull(HealthReport::getSubjectId)
|
|
|
+ .eq(HealthReport::getStatus, "active")
|
|
|
+ .orderByDesc(HealthReport::getReportDate)
|
|
|
+ );
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**验证命令**:
|
|
|
+```bash
|
|
|
+mvn clean compile # 必须通过编译
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 4:编写管理端 HealthReportReassign.vue 页面
|
|
|
+
|
|
|
+**文件**:`/sc-data/cfc/cfc-web/src/views/admin/HealthReportReassign.vue`
|
|
|
+
|
|
|
+**位置**:新建文件,遵循现有 admin 页面风格(复用 HealthReportAudit.vue 等组件)
|
|
|
+
|
|
|
+**代码块**(完整组件):
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div class="reassign-container">
|
|
|
+ <div class="toolbar">
|
|
|
+ <span>报告归属管理</span>
|
|
|
+ <a-button @click="refresh">刷新</a-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <a-table
|
|
|
+ :data="reports"
|
|
|
+ :columns="columns"
|
|
|
+ :loading="loading"
|
|
|
+ :row-key="row => row.id"
|
|
|
+ @selection-change="handleSelectionChange"
|
|
|
+ >
|
|
|
+ <template #row-actions="{ row }">
|
|
|
+ <template v-if="row.subjectId">
|
|
|
+ <!-- 已绑定:显示重新分配按钮 -->
|
|
|
+ <a-dropdown v-dropdown="dropdown">
|
|
|
+ <a-trigger>
|
|
|
+ <a-icon type="setting" />
|
|
|
+ </a-trigger>
|
|
|
+ <a-menu>
|
|
|
+ <a-menu-item @click="reassignRow(row)">重新分配</a-menu-item>
|
|
|
+ </a-menu>
|
|
|
+ </a-dropdown>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <!-- 未绑定:显示绑定按钮 -->
|
|
|
+ <a-button type="primary" size="small" @click="bindRow(row)">绑定归属</a-button>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </a-table>
|
|
|
+
|
|
|
+ <!-- 选择成员弹窗 -->
|
|
|
+ <a-modal :visible="visible" @ok="doReassign" @cancel="visible = false">
|
|
|
+ <template #title>
|
|
|
+ <span v="reassignMode">报告重新分配</span>
|
|
|
+ <span v-else>绑定归属</span>
|
|
|
+ </template>
|
|
|
+ <template #body>
|
|
|
+ <a-space direction="vertical" :space="8">
|
|
|
+ <a-select
|
|
|
+ :options="members"
|
|
|
+ placeholder="选择家庭成员"
|
|
|
+ @change="onMemberSelect"
|
|
|
+ disabled="reassignMode && !row.subjectId"
|
|
|
+ />
|
|
|
+ </a-space>
|
|
|
+ <!-- 如为重新分配模式,显示当前归属 -->
|
|
|
+ <template v-if="reassignMode && row.subjectId">
|
|
|
+ <p class="hint-text">当前归属:{{ getMemberName(row.subjectId) }}</p>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ <footer>
|
|
|
+ <a-button @click="visible = false">取消</a-button>
|
|
|
+ <a-button type="primary" @click="doConfirm">确认</a-button>
|
|
|
+ </footer>
|
|
|
+ </a-modal>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, watch } from 'vue'
|
|
|
+import { aTable, aSelect, aModal, aButton, aSpace, aMessage } from '/umi-design'
|
|
|
+import { request } from '/utils/api'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ familyId: { type: Number, required: true }
|
|
|
+})
|
|
|
+
|
|
|
+const members = ref([]) // 从 API 获取家庭成员列表
|
|
|
+const reports = ref([])
|
|
|
+const loading = ref(false)
|
|
|
+const visible = ref(false)
|
|
|
+let currentRow = null
|
|
|
+let reassignMode = false // true = 重新分配,false = 绑定
|
|
|
+
|
|
|
+// 列定义
|
|
|
+const columns = [
|
|
|
+ { title: '报告ID', dataIndex: 'id', width: 120 },
|
|
|
+ { title: '上传日期', dataIndex: 'reportDate', width: 140 },
|
|
|
+ { title: '报告类型', dataIndex: 'reportType', width: 100 },
|
|
|
+ { title: '主体', dataIndex: 'subjectDisplay', width: 120 },
|
|
|
+ { title: '操作', dataIndex: 'actions', width: 140 }
|
|
|
+]
|
|
|
+
|
|
|
+// 从后端获取成员列表
|
|
|
+const fetchMembers = async () => {
|
|
|
+ const { data } = await request('/api/family/member/list', {
|
|
|
+ includeFamily: true
|
|
|
+ })
|
|
|
+ members.value = data.result || data.list || []
|
|
|
+}
|
|
|
+
|
|
|
+// 刷新列表
|
|
|
+const refresh = async () => {
|
|
|
+ loading.value = true
|
|
|
+ const { data } = await request('/api/health/report/unlinked', { familyId: props.familyId })
|
|
|
+ reports.value = data.result || data
|
|
|
+ loading.value = false
|
|
|
+ await fetchMembers()
|
|
|
+}
|
|
|
+
|
|
|
+// 绑定/重新分配行
|
|
|
+const bindRow = (row) => {
|
|
|
+ currentRow = row
|
|
|
+ reassignMode = false
|
|
|
+ visible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const reassignRow = (row) => {
|
|
|
+ currentRow = row
|
|
|
+ reassignMode = true
|
|
|
+ visible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 成员选择变化
|
|
|
+const onMemberSelect = (e) => {
|
|
|
+ // 选中后直接在 doConfirm 中使用 selectedMemberId
|
|
|
+}
|
|
|
+
|
|
|
+// 获取成员名称
|
|
|
+const getMemberName = (memberId) => {
|
|
|
+ const m = members.value.find(x => x.id === memberId)
|
|
|
+ return m ? (m.nickname || m.userId.toString()) : '未知'
|
|
|
+}
|
|
|
+
|
|
|
+// 确认操作
|
|
|
+const doConfirm = async () => {
|
|
|
+ if (currentRow.subjectId && reassignMode) {
|
|
|
+ // 重新分配:发送新的 subjectId
|
|
|
+ await request('/api/health/report/reassign', {
|
|
|
+ reportId: currentRow.id,
|
|
|
+ subjectId: selectedMemberId
|
|
|
+ })
|
|
|
+ } else if (!currentRow.subjectId && !reassignMode) {
|
|
|
+ // 绑定:发送当前可选的 memberId(此处需从 select 选项中获取)
|
|
|
+ // 简化处理:绑定到第一个可用成员
|
|
|
+ await request('/api/health/report/reassign', {
|
|
|
+ reportId: currentRow.id,
|
|
|
+ subjectId: selectedMemberId
|
|
|
+ })
|
|
|
+ }
|
|
|
+ visible.value = false
|
|
|
+ await refresh()
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.reassign-container { margin: 16px; }
|
|
|
+.hint-text { color: #888; font-size: 12px; margin-top: 4px; }
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+**验证命令**:
|
|
|
+```bash
|
|
|
+# 语法检查(Vue 文件无编译,确认无语法错误即可)
|
|
|
+# 通过 IDE 预览或运行项目启动检查
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 5:编写小程序报告列表增量改动
|
|
|
+
|
|
|
+**文件**:`/sc-data/cfc/cfc-frontend/pages/health/report-list.vue`
|
|
|
+
|
|
|
+**位置**:在报告卡片组件内,`subjectId === null` 时在右上角增加按钮
|
|
|
+
|
|
|
+**代码块**(在 report-item 组件内,靠近报告基础信息展示处):
|
|
|
+
|
|
|
+```vue
|
|
|
+<!-- 在报告基本信息区域的右上角 -->
|
|
|
+<view class="report-actions">
|
|
|
+ <view v-if="report.subjectId === null" class="action-btns">
|
|
|
+ <button type="primary" size="small" bindtap="adjustBinding">调整归属</button>
|
|
|
+ </view>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+**逻辑处理**(在页面 script 中添加):
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 调整归属(小程序端)
|
|
|
+const adjustBinding = () => {
|
|
|
+ // 弹出家庭成员选择器
|
|
|
+ wx.showActionSheet({
|
|
|
+ itemList: ['绑定新成员', '解除归属'],
|
|
|
+ itemColor: '#1890ff',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.tapIndex === 0) {
|
|
|
+ // 绑定新成员:弹出选择器并调用后端
|
|
|
+ wx.showModal({
|
|
|
+ content: '选择归属成员',
|
|
|
+ confirmText: '确定',
|
|
|
+ success: (modalRes) => {
|
|
|
+ // 此处应跳转到成员选择页面或调用 API
|
|
|
+ // 简化:直接提示
|
|
|
+ aMessage.info('功能待前端完整实现,请在管理端操作')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 解除归属:subjectId 设为 null
|
|
|
+ wx.request({
|
|
|
+ url: '/api/health/report/reassign',
|
|
|
+ method: 'POST',
|
|
|
+ data: {
|
|
|
+ reportId: report.id,
|
|
|
+ subjectId: null
|
|
|
+ },
|
|
|
+ success: (resp) => {
|
|
|
+ if (resp.data.code === 0) {
|
|
|
+ aMessage.success('归属已解除')
|
|
|
+ // 刷新当前页面或返回上一级
|
|
|
+ wx.navigateBack()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**验证命令**:
|
|
|
+```bash
|
|
|
+# 小程序语法检查通过 wxss/wxml 编译器验证
|
|
|
+# 实际开发中使用微信开发者工具进行 preview 和 compile
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 6:编写单元测试
|
|
|
+
|
|
|
+**文件**:`/sc-data/cfc/cfc-backend/src/test/java/com/etotem/cfc/service/HealthReportServiceTest.java`
|
|
|
+
|
|
|
+**新增测试用例**:
|
|
|
+
|
|
|
+```java
|
|
|
+@Test
|
|
|
+void testReassignReport_UnbindThenRebind() {
|
|
|
+ // 1. 先上传一份无归属报告(subjectId = null)
|
|
|
+ // 2. 调用 reassignReport 将其解绑(newSubjectId = null,旧值已为 null,直接返回或无操作)
|
|
|
+ // 3. 再次调用 reassignReport 绑定到特定成员
|
|
|
+ // 4. 验证数据库 subject_id 已更新
|
|
|
+ // 5. 验证 dimensionScoreService.refreshFromGutReport 被调用
|
|
|
+ // 6. 验证 foodRecommendService.recalculateForUser 被调用
|
|
|
+
|
|
|
+ // 由于需要 Mock 依赖,重点测试:事务边界、权限检查、SQL 更新语句
|
|
|
+ HealthReport report = new HealthReport();
|
|
|
+ report.setId(1L);
|
|
|
+ report.setFamilyId(100L);
|
|
|
+ report.setSubjectId(200L);
|
|
|
+
|
|
|
+ // Test: 无实际变化直接返回
|
|
|
+ when(report.getSubjectId()).thenReturn(200L);
|
|
|
+ reassignReport(1L, 200L, 100L);
|
|
|
+ // 验证未更新(无变化快路径)
|
|
|
+ verify(jdbcTemplate, never()).update(anyString(), anyLong(), anyLong());
|
|
|
+
|
|
|
+ // Test: 实际变更 - 解绑
|
|
|
+ reassignReport(1L, null, 100L);
|
|
|
+ verify(jdbcTemplate).update(anyString(), eq(null), eq(1L));
|
|
|
+ verify(dimensionScoreService).refreshFromGutReport(eq(200L), eq(1L));
|
|
|
+ verify(foodRecommendService).recalculateForUser(eq(200L));
|
|
|
+
|
|
|
+ // Test: 实际变更 - 绑定到新成员
|
|
|
+ reassignReport(1L, 300L, 100L);
|
|
|
+ verify(jdbcTemplate).update(anyString(), eq(300L), eq(1L));
|
|
|
+ verify(dimensionScoreService).refreshFromGutReport(eq(300L), eq(1L));
|
|
|
+ verify(foodRecommendService).recalculateForUser(eq(300L));
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**验证命令**:
|
|
|
+```bash
|
|
|
+mvn test -Dtest=HealthReportServiceTest # 运行测试,必须全部通过
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 7:文档更新
|
|
|
+
|
|
|
+**文件**:`docs/superpowers/api/API_REFERENCE.md`
|
|
|
+
|
|
|
+**位置**:在现有健康报告章节之后,新增重新分配归属接口文档
|
|
|
+
|
|
|
+**代码块**(在 `docs/superpowers/api/API_REFERENCE.md` 中添加):
|
|
|
+
|
|
|
+```markdown
|
|
|
+### 健康报告归属管理
|
|
|
+
|
|
|
+| 方法 | 路径 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `POST` | `/api/health/report/reassign` | 重新分配报告归属(含维度重算)<br>`{reportId, subjectId}`<br>`subjectId` 可为 null 表示解绑 |
|
|
|
+| `POST` | `/api/health/report/unlinked` | 获取家庭内所有未绑定归属的报告<br>**(注:查询条件已修复,现正确使用 family_id 字段)** |
|
|
|
+
|
|
|
+**使用示例**:
|
|
|
+
|
|
|
+```bash
|
|
|
+# 重新分配报告归属到成员 ID 300
|
|
|
+curl -X POST http://localhost:9082/api/health/report/reassign \
|
|
|
+ -H "Authorization: Bearer {token}" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{"reportId": 1, "subjectId": 300}'
|
|
|
+
|
|
|
+# 解除报告归属
|
|
|
+curl -X POST http://localhost:9082/api/health/report/reassign \
|
|
|
+ -H "Authorization: Bearer {token}" \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{"reportId": 1, "subjectId": null}'
|
|
|
+```
|
|
|
+
|
|
|
+**权限说明**:
|
|
|
+- 需要登录态(JWT Token),`familyId` 由 `JwtInterceptor` 从 Token 中反查得到
|
|
|
+- 仅当报告的 `familyId` 等于当前用户家庭 ID 时,操作才有效
|
|
|
+- 管理员/规划师可对家庭内任何报告进行归属管理
|
|
|
+
|
|
|
+**变更记录**:
|
|
|
+- `2026-10-05`: 新增 `/api/health/report/reassign` 接口,并修复 `/api/health/report/unlinked` 查询条件 bug(familyId→family_id)
|