Enable reassigning report ownership (subject_id) for health reports. Reports can exist in two states:
subject_id set to a family member ID — used for plan generation and dimension calculationssubject_id is null — not used for plan generation, can be reassigned to a memberhealth_reports.subject_id already exists, nullable — no code change needed for the fieldtriggerAutoPlanAfterConfirm already skips plan generation when subjectId == null/api/health/report/unlinked passes familyId but service queries by user_id/report/editPOST /api/health/report/reassign endpointController (HealthReportController.java):
@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("归属已更新");
}
Service (HealthReportService.java):
@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);
// 2. 重算旧成员维度(如果原来有绑定)
if (oldSubjectId != null) {
dimensionScoreService.refreshFromGutReport(oldSubjectId, reportId);
foodRecommendService.recalculateForUser(oldSubjectId);
}
// 3. 重算新成员维度(如果有新归属)
if (newSubjectId != null) {
dimensionScoreService.refreshFromGutReport(newSubjectId, reportId);
foodRecommendService.recalculateForUser(newSubjectId);
}
// 4. 日志记录
String operator = Spel.eval("#{requestAttributes.currentAuthentication.principal}", String.class);
log.info("报告归属变更: reportId={}, oldSubjectId={}, newSubjectId={}, operator={}",
reportId, oldSubjectId, newSubjectId, operator);
}
getUnlinkedReports query bugService (HealthReportService.java): Change method signature and query:
// Before (bug): public List<HealthReport> getUnlinkedReports(Long userId) {
// ... .eq(HealthReport::getUserId, userId) ... // wrong: queries by upload user
// }
// After (fix): public List<HealthReport> getUnlinkedReports(Long familyId) {
// return healthReportMapper.selectList(
// new LambdaQueryWrapper<HealthReport>()
// .eq(HealthReport::getFamilyId, familyId) // ← 改为 familyId
// .isNull(HealthReport::getSubjectId)
// .eq(HealthReport::getStatus, "active")
// );
// }
Controller (HealthReportController.java): No change needed — already passes familyId parameter.
文件: cfc-web/src/views/admin/HealthReportReassign.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"
@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="row.subjectId">{{ row.title }} - 重新分配</span>
<span v-else>绑定归属</span>
</template>
<template #body>
<a-space direction="vertical" :space="8">
<a-select
v-if="mode === 'reassign'"
:options="members"
placeholder="选择新归属成员"
@change="onMemberSelect"
/>
<a-select
v-else
:options="members"
placeholder="选择归属成员"
@change="onMemberSelect"
/>
</a-space>
</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'
const props = defineProps({
familyId: { type: Number, required: true }
})
const members = ref([]) // 从 API 获取家庭成员列表
const reports = ref([]) // 列表数据
const loading = ref(false)
const columns = [...]
const visible = ref(false)
let currentRow = null
let mode = 'bind' // 'bind' | 'reassign'
// 刷新列表
const refresh = async () => {
loading.value = true
const { data } = await request('/api/health/report/unlinked', { familyId: props.familyId })
reports.value = data
loading.value = false
}
// 绑定/重新分配行
const bindRow = (row) => {
currentRow = row
mode = 'bind'
visible.value = true
}
const reassignRow = (row) => {
currentRow = row
mode = 'reassign'
visible.value = true
}
// 成员选择变化
const onMemberSelect = (e) => { ... }
// 确认操作
const doConfirm = async () => {
if (mode === 'bind') {
await request('/api/health/report/bind', {
reportId: currentRow.id,
subjectId: currentRow.subjectId // 需要从选项中获取
})
} else {
await request('/api/health/report/reassign', {
reportId: currentRow.id,
subjectId: selectedMemberId
})
}
visible.value = false
await refresh()
}
</script>
页面: cfc-frontend/pages/health/report-list.vue
在报告卡片右上角增加「调整归属」按钮(仅在 subjectId === null 时显示):
<!-- 在 report-item 组件内 -->
<view v-if="report.subjectId === null">
<view class="action-btns">
<button type="primary" bindtap="adjustBinding">调整归属</button>
</view>
</view>
逻辑: 弹出家庭成员选择器,选择后调用后端新接口。
subject_id 已存在且为 nullable,schema.sql 无需迁移。
| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/api/health/report/reassign |
重新分配报告归属(含维度重算) |
GET |
/api/health/report/unlinked |
获取家庭内所有无归属报告(已修复查询 bug) |
subject_id 已在 schema.sql 中定义,无需迁移subjectId == null 报告,无需额外改动getUnlinkedReports 查询 bug 已修复(familyId → family_id)familyId 验证subject_id 为 0 或负值,需在 service 中统一视为 null 处理familyId 验证报告归属,防止跨家庭操作subject_id 为 null 的报告在生成方案时已被自动跳过,本设计不改变此行为POST /api/health/report/reassign 接口getUnlinkedReports 查询条件(familyId → family_id)HealthReportReassign.vue 页面