514 lines
20 KiB
PHP
514 lines
20 KiB
PHP
<?php
|
|||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace App\Controllers;
|
||
|
|
|
||
|
|
use App\Access;
|
||
|
|
use App\Auth;
|
||
|
|
use App\Comments;
|
||
|
|
use App\Database;
|
||
|
|
use App\Expenses;
|
||
|
|
use App\Members;
|
||
|
|
|
||
|
|
class NodeController
|
||
|
|
{
|
||
|
|
/** 计划节点只可能有 planned / completed / cancelled */
|
||
|
|
private const PLAN_STATUSES = ['planned', 'completed', 'cancelled'];
|
||
|
|
/** 执行节点本身就是"已完成",额外允许 cancelled */
|
||
|
|
private const REALITY_STATUSES = ['completed', 'cancelled', 'ongoing', 'planned'];
|
||
|
|
|
||
|
|
public function create(): void
|
||
|
|
{
|
||
|
|
$db = Database::get();
|
||
|
|
$activityId = (int)($_GET['activity_id'] ?? 0);
|
||
|
|
$category = $_GET['category'] ?? 'plan';
|
||
|
|
$category = in_array($category, ['plan', 'reality'], true) ? $category : 'plan';
|
||
|
|
|
||
|
|
$activity = $this->fetchOwnActivity($activityId);
|
||
|
|
|
||
|
|
view('nodes/form', $this->formData($db, $activity, $category, null));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function edit(): void
|
||
|
|
{
|
||
|
|
$db = Database::get();
|
||
|
|
$node = $this->fetchOwnNode((int)($_GET['id'] ?? 0));
|
||
|
|
|
||
|
|
$stmt = $db->prepare('SELECT * FROM activities WHERE id = ?');
|
||
|
|
$stmt->execute([$node['activity_id']]);
|
||
|
|
$activity = $stmt->fetch();
|
||
|
|
|
||
|
|
view('nodes/form', $this->formData($db, $activity, $node['node_category'], $node));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function store(): void
|
||
|
|
{
|
||
|
|
csrf_check();
|
||
|
|
$db = Database::get();
|
||
|
|
|
||
|
|
$activity = $this->fetchOwnActivity((int)($_POST['activity_id'] ?? 0));
|
||
|
|
$category = $_POST['node_category'] ?? 'plan';
|
||
|
|
if (!in_array($category, ['plan', 'reality'], true)) {
|
||
|
|
exit('无效的节点分类');
|
||
|
|
}
|
||
|
|
|
||
|
|
$input = $this->collectInput($db, $activity, $category);
|
||
|
|
if ($input['error'] !== null) {
|
||
|
|
flash('error', $input['error']);
|
||
|
|
redirect('nodes/create', ['activity_id' => $activity['id'], 'category' => $category]);
|
||
|
|
}
|
||
|
|
|
||
|
|
$data = $input['data'];
|
||
|
|
$sql = 'INSERT INTO trip_nodes
|
||
|
|
(activity_id, node_type_id, node_category, original_plan_node_id,
|
||
|
|
start_time, end_time, duration_minutes,
|
||
|
|
start_location, end_location,
|
||
|
|
title, description, travel_mode_id, vehicle_id,
|
||
|
|
line_name, carrier, trip_number, seat_info, gate, status, cancel_reason)
|
||
|
|
VALUES
|
||
|
|
(:activity_id, :node_type_id, :node_category, :original_plan_node_id,
|
||
|
|
:start_time, :end_time, :duration_minutes,
|
||
|
|
:start_location, :end_location,
|
||
|
|
:title, :description, :travel_mode_id, :vehicle_id,
|
||
|
|
:line_name, :carrier, :trip_number, :seat_info, :gate, :status, :cancel_reason)';
|
||
|
|
$stmt = $db->prepare($sql);
|
||
|
|
$stmt->execute($data);
|
||
|
|
|
||
|
|
flash('error', '节点已保存');
|
||
|
|
redirect('activities/show', ['id' => (int)$activity['id']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function update(): void
|
||
|
|
{
|
||
|
|
csrf_check();
|
||
|
|
$db = Database::get();
|
||
|
|
|
||
|
|
$node = $this->fetchOwnNode((int)($_POST['id'] ?? 0));
|
||
|
|
$category = $node['node_category'];
|
||
|
|
|
||
|
|
$stmt = $db->prepare('SELECT * FROM activities WHERE id = ?');
|
||
|
|
$stmt->execute([$node['activity_id']]);
|
||
|
|
$activity = $stmt->fetch();
|
||
|
|
|
||
|
|
$input = $this->collectInput($db, $activity, $category, $node);
|
||
|
|
if ($input['error'] !== null) {
|
||
|
|
flash('error', $input['error']);
|
||
|
|
redirect('nodes/edit', ['id' => (int)$node['id']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
$data = $input['data'];
|
||
|
|
unset($data['activity_id'], $data['node_category'], $data['original_plan_node_id']);
|
||
|
|
$data['id'] = (int)$node['id'];
|
||
|
|
|
||
|
|
$sql = 'UPDATE trip_nodes SET
|
||
|
|
node_type_id = :node_type_id,
|
||
|
|
start_time = :start_time, end_time = :end_time, duration_minutes = :duration_minutes,
|
||
|
|
start_location = :start_location, end_location = :end_location,
|
||
|
|
title = :title, description = :description,
|
||
|
|
travel_mode_id = :travel_mode_id, vehicle_id = :vehicle_id,
|
||
|
|
line_name = :line_name, carrier = :carrier, trip_number = :trip_number,
|
||
|
|
seat_info = :seat_info, gate = :gate, status = :status, cancel_reason = :cancel_reason
|
||
|
|
WHERE id = :id';
|
||
|
|
$stmt = $db->prepare($sql);
|
||
|
|
$stmt->execute($data);
|
||
|
|
|
||
|
|
flash('error', '保存成功');
|
||
|
|
redirect('activities/show', ['id' => (int)$node['activity_id']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 卡片上的状态快捷切换(计划节点:标记完成 / 撤销完成)
|
||
|
|
*/
|
||
|
|
public function status(): void
|
||
|
|
{
|
||
|
|
csrf_check();
|
||
|
|
$db = Database::get();
|
||
|
|
|
||
|
|
$node = $this->fetchOwnNode((int)($_POST['id'] ?? 0));
|
||
|
|
$status = (string)($_POST['status'] ?? '');
|
||
|
|
|
||
|
|
$allowed = $node['node_category'] === 'plan' ? self::PLAN_STATUSES : self::REALITY_STATUSES;
|
||
|
|
if (!in_array($status, $allowed, true)) {
|
||
|
|
flash('error', '无效的状态');
|
||
|
|
redirect('activities/show', ['id' => (int)$node['activity_id']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
$reason = trim($_POST['cancel_reason'] ?? '');
|
||
|
|
$stmt = $db->prepare('UPDATE trip_nodes SET status = ?, cancel_reason = ? WHERE id = ?');
|
||
|
|
$stmt->execute([
|
||
|
|
$status,
|
||
|
|
$status === 'cancelled' ? ($reason !== '' ? $reason : null) : null,
|
||
|
|
(int)$node['id'],
|
||
|
|
]);
|
||
|
|
|
||
|
|
$labels = ['planned' => '已恢复为待执行', 'completed' => '已标记为完成', 'cancelled' => '已取消', 'ongoing' => '已标记为进行中'];
|
||
|
|
flash('error', $labels[$status] ?? '状态已更新');
|
||
|
|
redirect('activities/show', ['id' => (int)$node['activity_id']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 用计划节点生成一条执行记录(复制字段,保留 original_plan_node_id 指向计划节点)
|
||
|
|
*/
|
||
|
|
public function fromPlan(): void
|
||
|
|
{
|
||
|
|
csrf_check();
|
||
|
|
$db = Database::get();
|
||
|
|
|
||
|
|
$plan = $this->fetchOwnNode((int)($_POST['plan_node_id'] ?? 0));
|
||
|
|
if ($plan['node_category'] !== 'plan') {
|
||
|
|
flash('error', '只有计划节点可以生成为执行记录');
|
||
|
|
redirect('activities/show', ['id' => (int)$plan['activity_id']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 已经生成过就不重复生成
|
||
|
|
$chk = $db->prepare('SELECT id FROM trip_nodes WHERE original_plan_node_id = ? LIMIT 1');
|
||
|
|
$chk->execute([(int)$plan['id']]);
|
||
|
|
$exists = $chk->fetchColumn();
|
||
|
|
if ($exists) {
|
||
|
|
flash('error', '该计划节点已经有执行记录了');
|
||
|
|
redirect('activities/show', ['id' => (int)$plan['activity_id']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
$stmt = $db->prepare('INSERT INTO trip_nodes
|
||
|
|
(activity_id, node_type_id, node_category, original_plan_node_id,
|
||
|
|
start_time, end_time, duration_minutes,
|
||
|
|
start_location, end_location,
|
||
|
|
title, description, travel_mode_id, vehicle_id,
|
||
|
|
line_name, carrier, trip_number, seat_info, gate, status)
|
||
|
|
VALUES (?, ?, "reality", ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "completed")');
|
||
|
|
$stmt->execute([
|
||
|
|
(int)$plan['activity_id'],
|
||
|
|
(int)$plan['node_type_id'],
|
||
|
|
(int)$plan['id'],
|
||
|
|
$plan['start_time'],
|
||
|
|
$plan['end_time'],
|
||
|
|
$plan['duration_minutes'],
|
||
|
|
$plan['start_location'],
|
||
|
|
$plan['end_location'],
|
||
|
|
$plan['title'],
|
||
|
|
$plan['description'],
|
||
|
|
$plan['travel_mode_id'],
|
||
|
|
$plan['vehicle_id'],
|
||
|
|
$plan['line_name'],
|
||
|
|
$plan['carrier'],
|
||
|
|
$plan['trip_number'],
|
||
|
|
$plan['seat_info'],
|
||
|
|
$plan['gate'],
|
||
|
|
]);
|
||
|
|
|
||
|
|
flash('error', '已根据计划节点生成执行记录,可继续编辑补充实际情况');
|
||
|
|
redirect('activities/show', ['id' => (int)$plan['activity_id']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function destroy(): void
|
||
|
|
{
|
||
|
|
csrf_check();
|
||
|
|
$db = Database::get();
|
||
|
|
|
||
|
|
$node = $this->fetchOwnNode((int)($_POST['id'] ?? 0));
|
||
|
|
|
||
|
|
// 软关联的附件先查出来,删完数据再删文件
|
||
|
|
$stmt = $db->prepare('SELECT file_url FROM attachments
|
||
|
|
WHERE target_type = "trip_node" AND target_id = ?');
|
||
|
|
$stmt->execute([(int)$node['id']]);
|
||
|
|
$files = $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||
|
|
|
||
|
|
$stmt = $db->prepare('DELETE FROM attachments WHERE target_type = "trip_node" AND target_id = ?');
|
||
|
|
$stmt->execute([(int)$node['id']]);
|
||
|
|
|
||
|
|
$stmt = $db->prepare('DELETE FROM comments WHERE target_type = "trip_node" AND target_id = ?');
|
||
|
|
$stmt->execute([(int)$node['id']]);
|
||
|
|
|
||
|
|
// 若有执行记录指向本计划节点,外键是 ON DELETE SET NULL,会自动置空
|
||
|
|
$stmt = $db->prepare('DELETE FROM trip_nodes WHERE id = ?');
|
||
|
|
$stmt->execute([(int)$node['id']]);
|
||
|
|
|
||
|
|
foreach ($files as $url) {
|
||
|
|
$this->deleteAttachmentFile((string)$url);
|
||
|
|
}
|
||
|
|
|
||
|
|
flash('error', '节点已删除');
|
||
|
|
redirect('activities/show', ['id' => (int)$node['activity_id']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 节点详情页:把挂在这个节点上的参与人、花销、讨论集中到一页
|
||
|
|
*/
|
||
|
|
public function show(): void
|
||
|
|
{
|
||
|
|
$db = Database::get();
|
||
|
|
$nodeId = (int)($_GET['id'] ?? 0);
|
||
|
|
|
||
|
|
$node = Access::node($nodeId); // 含 family_id / activity_id / activity_title
|
||
|
|
|
||
|
|
// 节点类型(本家庭)
|
||
|
|
$stmt = $db->prepare('SELECT name, icon FROM node_types WHERE id = ?');
|
||
|
|
$stmt->execute([(int)$node['node_type_id']]);
|
||
|
|
$type = $stmt->fetch();
|
||
|
|
if (!$type) $type = ['name' => '未分类', 'icon' => '·'];
|
||
|
|
|
||
|
|
// 出行方式(连同车辆信息拼成一行展示文本)
|
||
|
|
$travelModeLabel = '';
|
||
|
|
if (!empty($node['travel_mode_id'])) {
|
||
|
|
$stmt = $db->prepare('
|
||
|
|
SELECT tm.mode_type, v.plate_number, v.model_name
|
||
|
|
FROM travel_modes tm
|
||
|
|
LEFT JOIN vehicles v ON v.id = tm.vehicle_id
|
||
|
|
WHERE tm.id = ?');
|
||
|
|
$stmt->execute([(int)$node['travel_mode_id']]);
|
||
|
|
$tm = $stmt->fetch();
|
||
|
|
if ($tm) {
|
||
|
|
$label = TravelModeController::MODE_LABELS[$tm['mode_type']] ?? $tm['mode_type'];
|
||
|
|
if (!empty($tm['plate_number'])) {
|
||
|
|
$label .= ' · ' . $tm['plate_number'];
|
||
|
|
} elseif (!empty($tm['model_name'])) {
|
||
|
|
$label .= ' · ' . $tm['model_name'];
|
||
|
|
}
|
||
|
|
// 线路名属于节点自身(trip_nodes.line_name),不是出行方式表的列
|
||
|
|
if (!empty($node['line_name'])) {
|
||
|
|
$label .= ' · ' . $node['line_name'];
|
||
|
|
}
|
||
|
|
$travelModeLabel = $label;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$stmt = $db->prepare('SELECT COUNT(*) FROM attachments
|
||
|
|
WHERE target_type = "trip_node" AND target_id = ?');
|
||
|
|
$stmt->execute([$nodeId]);
|
||
|
|
$attachmentCount = (int)$stmt->fetchColumn();
|
||
|
|
|
||
|
|
$familyId = (int)$node['family_id'];
|
||
|
|
|
||
|
|
view('nodes/show', [
|
||
|
|
'node' => array_merge($node, [
|
||
|
|
'node_type_name' => (string)$type['name'],
|
||
|
|
'node_type_icon' => (string)$type['icon'],
|
||
|
|
'travel_mode_label' => $travelModeLabel,
|
||
|
|
]),
|
||
|
|
'attachmentCount' => $attachmentCount,
|
||
|
|
'members' => Members::forNode($nodeId),
|
||
|
|
'familyUsers' => Members::familyUsers($familyId),
|
||
|
|
'roleTags' => Members::roleTagMap($familyId),
|
||
|
|
'emotions' => Members::EMOTIONS,
|
||
|
|
'expenses' => Expenses::forNode($nodeId),
|
||
|
|
'expenseSummary' => Expenses::summaryForNode($nodeId),
|
||
|
|
'categories' => Expenses::CATEGORIES,
|
||
|
|
'currencies' => Expenses::currencyLabels(),
|
||
|
|
'expenseSymbols' => Expenses::SYMBOLS,
|
||
|
|
'currentUserId' => (int)Auth::id(),
|
||
|
|
'commentTree' => Comments::tree(Comments::TARGET_NODE, $nodeId),
|
||
|
|
'pageTitle' => $node['title'] !== null && $node['title'] !== ''
|
||
|
|
? (string)$node['title']
|
||
|
|
: '节点详情',
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ------------------------------------------------------------------
|
||
|
|
// 内部辅助
|
||
|
|
// ------------------------------------------------------------------
|
||
|
|
|
||
|
|
/** 组装 nodes/form 模板所需数据 */
|
||
|
|
private function formData(\PDO $db, array $activity, string $category, ?array $node): array
|
||
|
|
{
|
||
|
|
$stmt = $db->prepare('SELECT * FROM node_types WHERE family_id = ? ORDER BY is_system DESC, id ASC');
|
||
|
|
$stmt->execute([$activity['family_id']]);
|
||
|
|
$nodeTypes = $stmt->fetchAll();
|
||
|
|
|
||
|
|
$stmt = $db->prepare('
|
||
|
|
SELECT tm.*, v.plate_number, v.model_name
|
||
|
|
FROM travel_modes tm
|
||
|
|
LEFT JOIN vehicles v ON v.id = tm.vehicle_id
|
||
|
|
WHERE tm.family_id = ?
|
||
|
|
ORDER BY FIELD(tm.mode_type, "drive_own","drive_rental","train","plane","bus","subway","taxi","bike","walk","ship","other"), tm.id ASC
|
||
|
|
');
|
||
|
|
$stmt->execute([$activity['family_id']]);
|
||
|
|
$travelModes = $stmt->fetchAll();
|
||
|
|
|
||
|
|
// 执行节点可以关联计划节点(编辑已有执行节点时不再改关联关系)
|
||
|
|
$planNodes = [];
|
||
|
|
if ($category === 'reality' && $node === null) {
|
||
|
|
$s = $db->prepare('SELECT id, title, start_time FROM trip_nodes WHERE activity_id = ? AND node_category = "plan" ORDER BY start_time');
|
||
|
|
$s->execute([$activity['id']]);
|
||
|
|
$planNodes = $s->fetchAll();
|
||
|
|
}
|
||
|
|
|
||
|
|
$isEdit = $node !== null;
|
||
|
|
|
||
|
|
return [
|
||
|
|
'modeLabels' => TravelModeController::MODE_LABELS,
|
||
|
|
'activity' => $activity,
|
||
|
|
'category' => $category,
|
||
|
|
'nodeTypes' => $nodeTypes,
|
||
|
|
'travelModes' => $travelModes,
|
||
|
|
'planNodes' => $planNodes,
|
||
|
|
'node' => $node,
|
||
|
|
'isEdit' => $isEdit,
|
||
|
|
'pageTitle' => ($isEdit ? '编辑' : '新建') . ($category === 'plan' ? '计划' : '执行') . '节点',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 校验并整理表单输入
|
||
|
|
* @return array{error: ?string, data: array}
|
||
|
|
*/
|
||
|
|
private function collectInput(\PDO $db, array $activity, string $category, ?array $node = null): array
|
||
|
|
{
|
||
|
|
$startTime = $this->normalizeDateTime((string)($_POST['start_time'] ?? ''));
|
||
|
|
$endTime = $this->normalizeDateTime((string)($_POST['end_time'] ?? ''));
|
||
|
|
if ($startTime === null || $endTime === null) {
|
||
|
|
return ['error' => '开始和结束时间必填', 'data' => []];
|
||
|
|
}
|
||
|
|
if (strtotime($endTime) < strtotime($startTime)) {
|
||
|
|
return ['error' => '结束时间不能早于开始时间', 'data' => []];
|
||
|
|
}
|
||
|
|
|
||
|
|
$nodeTypeId = (int)($_POST['node_type_id'] ?? 0);
|
||
|
|
$chk = $db->prepare('SELECT id FROM node_types WHERE id = ? AND family_id = ?');
|
||
|
|
$chk->execute([$nodeTypeId, $activity['family_id']]);
|
||
|
|
if (!$chk->fetch()) {
|
||
|
|
return ['error' => '节点类型不存在', 'data' => []];
|
||
|
|
}
|
||
|
|
|
||
|
|
$travelModeId = !empty($_POST['travel_mode_id']) ? (int)$_POST['travel_mode_id'] : null;
|
||
|
|
$vehicleId = null;
|
||
|
|
if ($travelModeId) {
|
||
|
|
$s = $db->prepare('SELECT vehicle_id FROM travel_modes WHERE id = ? AND family_id = ?');
|
||
|
|
$s->execute([$travelModeId, $activity['family_id']]);
|
||
|
|
$row = $s->fetch();
|
||
|
|
if (!$row) {
|
||
|
|
return ['error' => '出行方式不存在', 'data' => []];
|
||
|
|
}
|
||
|
|
$vehicleId = $row['vehicle_id'] !== null ? (int)$row['vehicle_id'] : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 状态:计划节点默认待执行,执行节点默认已完成
|
||
|
|
$defaultStatus = $category === 'plan' ? 'planned' : 'completed';
|
||
|
|
$status = (string)($_POST['status'] ?? $defaultStatus);
|
||
|
|
$allowed = $category === 'plan' ? self::PLAN_STATUSES : self::REALITY_STATUSES;
|
||
|
|
if (!in_array($status, $allowed, true)) {
|
||
|
|
$status = $node['status'] ?? $defaultStatus;
|
||
|
|
}
|
||
|
|
|
||
|
|
$originalPlanId = null;
|
||
|
|
if ($category === 'reality' && $node === null && !empty($_POST['original_plan_node_id'])) {
|
||
|
|
$originalPlanId = (int)$_POST['original_plan_node_id'];
|
||
|
|
// 关联的计划节点必须属于同一个活动,否则视为未关联
|
||
|
|
$s = $db->prepare('SELECT id FROM trip_nodes WHERE id = ? AND activity_id = ? AND node_category = "plan"');
|
||
|
|
$s->execute([$originalPlanId, $activity['id']]);
|
||
|
|
if (!$s->fetch()) {
|
||
|
|
$originalPlanId = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$data = [
|
||
|
|
'activity_id' => (int)$activity['id'],
|
||
|
|
'node_type_id' => $nodeTypeId,
|
||
|
|
'node_category' => $category,
|
||
|
|
'original_plan_node_id' => $originalPlanId,
|
||
|
|
'start_time' => $startTime,
|
||
|
|
'end_time' => $endTime,
|
||
|
|
'duration_minutes' => $this->calcDuration($startTime, $endTime),
|
||
|
|
'start_location' => $this->buildLocation('start'),
|
||
|
|
'end_location' => $this->buildLocation('end'),
|
||
|
|
'title' => trim($_POST['title'] ?? ''),
|
||
|
|
'description' => trim($_POST['description'] ?? ''),
|
||
|
|
'travel_mode_id' => $travelModeId,
|
||
|
|
'vehicle_id' => $vehicleId,
|
||
|
|
'line_name' => $this->strOrNull('line_name'),
|
||
|
|
'carrier' => $this->strOrNull('carrier'),
|
||
|
|
'trip_number' => $this->strOrNull('trip_number'),
|
||
|
|
'seat_info' => $this->strOrNull('seat_info'),
|
||
|
|
'gate' => $this->strOrNull('gate'),
|
||
|
|
'status' => $status,
|
||
|
|
'cancel_reason' => $status === 'cancelled' ? $this->strOrNull('cancel_reason') : null,
|
||
|
|
];
|
||
|
|
|
||
|
|
return ['error' => null, 'data' => $data];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** datetime-local 传上来是 2026-09-12T10:30,统一成 MySQL 认的 "Y-m-d H:i:s" */
|
||
|
|
private function normalizeDateTime(string $value): ?string
|
||
|
|
{
|
||
|
|
$value = trim($value);
|
||
|
|
if ($value === '') return null;
|
||
|
|
$value = str_replace('T', ' ', $value);
|
||
|
|
$ts = strtotime($value);
|
||
|
|
if ($ts === false) return null;
|
||
|
|
return date('Y-m-d H:i:s', $ts);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function fetchOwnActivity(int $id): array
|
||
|
|
{
|
||
|
|
if ($id <= 0) {
|
||
|
|
http_response_code(404);
|
||
|
|
exit('活动不存在');
|
||
|
|
}
|
||
|
|
$stmt = Database::get()->prepare('SELECT * FROM activities WHERE id = ?');
|
||
|
|
$stmt->execute([$id]);
|
||
|
|
$activity = $stmt->fetch();
|
||
|
|
if (!$activity || (int)$activity['family_id'] !== Auth::familyId()) {
|
||
|
|
http_response_code(404);
|
||
|
|
exit('活动不存在');
|
||
|
|
}
|
||
|
|
return $activity;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function fetchOwnNode(int $id): array
|
||
|
|
{
|
||
|
|
if ($id <= 0) {
|
||
|
|
http_response_code(404);
|
||
|
|
exit('节点不存在');
|
||
|
|
}
|
||
|
|
$stmt = Database::get()->prepare('
|
||
|
|
SELECT tn.*, a.family_id
|
||
|
|
FROM trip_nodes tn
|
||
|
|
JOIN activities a ON a.id = tn.activity_id
|
||
|
|
WHERE tn.id = ?');
|
||
|
|
$stmt->execute([$id]);
|
||
|
|
$node = $stmt->fetch();
|
||
|
|
if (!$node || (int)$node['family_id'] !== Auth::familyId()) {
|
||
|
|
http_response_code(404);
|
||
|
|
exit('节点不存在');
|
||
|
|
}
|
||
|
|
return $node;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function deleteAttachmentFile(string $url): void
|
||
|
|
{
|
||
|
|
if (strpos($url, '/uploads/attachments/') !== 0 || strpos($url, '..') !== false) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
$file = dirname(__DIR__, 2) . $url;
|
||
|
|
if (is_file($file)) @unlink($file);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function calcDuration(string $start, string $end): int
|
||
|
|
{
|
||
|
|
$s = strtotime($start);
|
||
|
|
$e = strtotime($end);
|
||
|
|
return max(0, (int)round(($e - $s) / 60));
|
||
|
|
}
|
||
|
|
|
||
|
|
private function buildLocation(string $which): ?string
|
||
|
|
{
|
||
|
|
$name = trim($_POST["{$which}_location_name"] ?? '');
|
||
|
|
$address = trim($_POST["{$which}_location_address"] ?? '');
|
||
|
|
if ($name === '' && $address === '') return null;
|
||
|
|
|
||
|
|
$lat = $_POST["{$which}_location_lat"] ?? null;
|
||
|
|
$lng = $_POST["{$which}_location_lng"] ?? null;
|
||
|
|
return json_encode([
|
||
|
|
'name' => $name,
|
||
|
|
'address' => $address,
|
||
|
|
'lat' => ($lat === '' || $lat === null) ? null : $lat,
|
||
|
|
'lng' => ($lng === '' || $lng === null) ? null : $lng,
|
||
|
|
], JSON_UNESCAPED_UNICODE);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function strOrNull(string $key): ?string
|
||
|
|
{
|
||
|
|
$v = trim($_POST[$key] ?? '');
|
||
|
|
return $v === '' ? null : $v;
|
||
|
|
}
|
||
|
|
}
|