节点表单:参与人直接勾选 + 起点终点记录详细地址(批 13)

参与人(用户反馈:不想等全部填完、保存之后才回详情页加人):
- 节点表单里新增「参与人(直接勾选)」区块:家庭成员按胶囊复选框列出,
  保存即生效;编辑时预勾选已有人。
- Members::syncForNode() 只做增删,**不动留下来的那些人的情绪 / 签到 /
  角色标签**(重新勾一遍不该把之前记的心情清掉)。
- 安全性:新增走 Members::save(),逐条校验「用户属于该活动所在家庭」,
  夹带别家 user_id 进不来;只有请求带 members_present=1(表单确实渲染了
  参与人区块)才同步 —— 否则没这个字段的请求会把已有成员清空。

起点/终点记录详细地址(用户反馈:高德联想只返回简化地名):
- 联想项选中后不再是「青岛北站」就完事:
  ① 有坐标 → 逆地理编码(AMap.Geocoder.getAddress)取该点的
     formattedAddress;
  ② 没坐标 → 按「区县 + 名称」正查,顺带把坐标补回来。
- 详细地址写进 start/end_location 的 address 字段,输入框下方实时显示
  「详细地址:…」,节点详情页本来就展示这一项。
- 路线地图补点(只有地名没坐标时)改为**优先用详细地址**查询,
  比简化地名定位准。

已验证:本地全链 12 步全绿(渲染检查 350 项、真 AMap 浏览器验证 11 项、
表单交互 75 项);远端 e2e 350 项 0 失败(新增 10 项:勾选落库 / 跨家庭
拒绝 / 取消勾选移出 / 情绪不被清 / 无标记不动参与人);部署静态断言、
双 PHP 渲染、内容断言全过后提交。
This commit is contained in:
2026-09-24 12:36:01 +08:00
parent c4c3b17cf7
commit 142b187911
5 changed files with 254 additions and 4 deletions
+53 -1
View File
@@ -95,8 +95,13 @@ class NodeController
$stmt = $db->prepare($sql);
$stmt->execute($data);
$nodeId = (int)$db->lastInsertId();
// 节点必须先有 id 才能挂附件,所以附件在插入之后处理
$extra = $this->handleNodeUploads((int)$db->lastInsertId());
$extra = $this->handleNodeUploads($nodeId);
// 表单里勾的参与人(members_present=1 才认,避免没渲染这个区块的请求误清)
$extra .= $this->syncMembers($nodeId);
flash('error', '节点已保存' . $extra);
redirect('activities/show', ['id' => (int)$activity['id']]);
@@ -148,6 +153,9 @@ class NodeController
// 再处理这次新选的照片
$extra = $this->handleNodeUploads((int)$node['id']);
// 参与人在表单里直接勾(编辑时重新勾选不会清掉已记的情绪/签到)
$extra .= $this->syncMembers((int)$node['id']);
$msg = '保存成功';
if ($removed > 0) {
$msg .= ",删除了 {$removed} 个附件";
@@ -358,6 +366,18 @@ class NodeController
$isEdit = $node !== null;
/* 参与人在表单里直接勾(不必先保存再回详情页加人)。
编辑时把已有人带过去预勾选;新建时空着。 */
$familyUsers = Members::familyUsers((int)$activity['family_id']);
$nodeMemberIds = [];
if ($isEdit) {
$s = $db->prepare('SELECT user_id FROM node_members WHERE trip_node_id = ?');
$s->execute([(int)$node['id']]);
foreach ($s->fetchAll() as $row) {
$nodeMemberIds[] = (int)$row['user_id'];
}
}
return [
'modeLabels' => TravelModeController::MODE_LABELS,
'activity' => $activity,
@@ -367,6 +387,8 @@ class NodeController
'planNodes' => $planNodes,
'node' => $node,
'isEdit' => $isEdit,
'familyUsers' => $familyUsers,
'nodeMemberIds' => $nodeMemberIds,
// 编辑时把已有照片带进表单:能看、能勾选删除。新建时节点还没 id,
// 附件挂不上去,所以只能是空数组(模板里会提示先保存)。
'attachments' => $isEdit ? Attachments::forTarget(Attachments::TARGET_NODE, (int)$node['id']) : [],
@@ -583,6 +605,36 @@ class NodeController
* ⚠️ 必须逐条校验归属:attachments 是「target_type + target_id」的软关联,
* 表单里的 id 是用户可以随便改的,不校验就等于给了跨家庭删文件的入口。
*/
/**
* 表单里勾的参与人 → 同步成节点成员(只增删,不动已记的情绪/签到)。
*
* 只在表单确实渲染了参与人区块时(members_present=1)才同步:
* 没有渲染却收到空数组的话,会把节点已有成员全删掉。
* 跨家庭的 user_id 由 Members::save() 逐条拦掉。
*/
private function syncMembers(int $nodeId): string
{
if (($_POST['members_present'] ?? '') !== '1') {
return '';
}
$picked = $_POST['members'] ?? [];
if (!is_array($picked)) {
return '';
}
$res = Members::syncForNode($nodeId, $picked);
if ($res['added'] === 0 && $res['removed'] === 0) {
return '';
}
$parts = [];
if ($res['added'] > 0) {
$parts[] = "加入 {$res['added']} 位参与人";
}
if ($res['removed'] > 0) {
$parts[] = "移出 {$res['removed']} 位参与人";
}
return ',' . implode(',', $parts);
}
private function deleteCheckedAttachments(int $nodeId): int
{
$ids = $_POST['delete_attachments'] ?? [];