v62:影院管理补齐接口(统计 / 年度报告 / 高级搜索)+ 名册带工作经历
用户 2026-10-19:「设置里面的影院管理,这些功能都还没有,应该补充, 就是增加影院呢、修改影院呢、高级搜索、统计、年度报告,这些全部都应该补充进去。」 一、三个新子路由(都是**家族级**,必须在 `$id > 0` 分支之前处理) GET /api/v1/movies/stats 总览 + 按影院 / 按人 / 按月 / 按年 GET /api/v1/movies/report 年度报告(以登录者为中心,?year= 可切年) GET /api/v1/movies/search 片名 / 影院 / 人 / 起止日期的组合搜索 ⚠️ 全部复用**网页端那一份 `MovieStats`**(overview/byCinema/byPerson/byMonth/ byYear/annual/years/search)—— 统计数字两边必须一致,否则用户会发现 "网页 124 部、App 121 部"。 ⚠️ 三个路由都做了 **`$id === 0` 守卫**:`/movies/stats` 不能被当成"id=0 的详情"。 ⚠️ `search` 在条件全空时**不查全库**(返回 has_cond=0),与网页 `hasCond` 同行为 — 否则"搜索"等于"列出全部观影"。 二、features 声明 movies_stats / movies_report / movies_search 老端上据此隐藏入口(新端上不用探测,但让能力可发现是这套约定的一部分)。 三、名册带 `jobs`(用户:「名册的列表太空了,应该显示更多内容,例如工作单位、职务」) 新增 `PersonJob::mapFor(array $personIds)` —— **一次 `IN (…)` 批量取回**。 ⚠️ 不能在装饰循环里逐人 `PersonJob::list()`:名册上千人就是上千次查询(N+1)。 ⚠️ 排序与 `list()` 逐字一致(当前段在前 → 开始时间倒序 → id 倒序), 否则"列表显示的单位"和"详情里第一段"会是两个不同的单位。 ⚠️ 端上列表实际用的是 `persons.org` / `persons.title`(本地库有那两列), `jobs` 是给"接口直出"的场景预备的 —— 两条链路都保留。 验证:_fb_v62_cinema_api.py 45/0(独立副本 + 真 MariaDB); 覆盖影院 CRUD 10 条 + 统计 8 + 年报 7 + 搜索 7 + 名册带 jobs 4; php -l 全量通过。 线上:本提交对应的代码**已在线上**(13:17 那次部署带上去的,非本次操作), 真机实测统计页显示的是真实数据(横店电影城 4 / 上影影城 3…)。
This commit is contained in:
@@ -47,6 +47,12 @@ final class ApiHandler
|
||||
'account' => 1,
|
||||
'account_password' => 1,
|
||||
'home_memories' => 1,
|
||||
/* 🔴 v62:影院管理补齐(用户点名「增加影院/修改影院/高级搜索/
|
||||
统计/年度报告」)。端上可据此探测:老服务端没有这三条时
|
||||
只显示影院列表,不显示搜索/统计/报告的入口。 */
|
||||
'movies_stats' => 1,
|
||||
'movies_report' => 1,
|
||||
'movies_search' => 1,
|
||||
// v58:同步可以"先问一句哪些模块变了",只拉变了的
|
||||
// (见 `changes()` 的说明;老端上没有这个标志也不会坏)
|
||||
'changes' => 1,
|
||||
@@ -524,6 +530,12 @@ final class ApiHandler
|
||||
$row['initials'] = Pinyin::initials(
|
||||
(string)$row['name'] . ' ' . (string)($row['my_nickname'] ?? '')
|
||||
);
|
||||
/* 🔴 v62(用户 2026-10-19):「现在名册的列表太空了,应该显示更多内容,
|
||||
例如**工作单位、职务**。」
|
||||
⇒ 端上列表要显示"当前那段工作经历"的单位/职务。
|
||||
⚠️ 具体经历在下面**一次批量**取回(`PersonJob::mapFor`),
|
||||
这里只准备字段位——**绝不在这里逐人查库**(N+1)。 */
|
||||
$row['jobs'] = [];
|
||||
|
||||
// 🔴 脱敏:名册行同样是 `SELECT p.*`,敏感密文一律不下发
|
||||
// (见 stripSensitivePerson 的说明)。
|
||||
@@ -540,6 +552,15 @@ final class ApiHandler
|
||||
));
|
||||
}
|
||||
|
||||
/* 🔴 v62:**一次**把所有人的工作经历取回来(`IN (…)` 单条 SQL)——
|
||||
⚠️ 不能在装饰循环里逐人 `PersonJob::list()`:名册上千人就是上千次查询。
|
||||
排序与 `PersonJob::list()` 一致,端上取第一段即"当前/最近的那段"。 */
|
||||
$jobsOf = PersonJob::mapFor(array_map(static fn(array $r): int => (int)$r['id'], $rows));
|
||||
foreach ($rows as &$row2) {
|
||||
$row2['jobs'] = $jobsOf[(int)$row2['id']] ?? [];
|
||||
}
|
||||
unset($row2);
|
||||
|
||||
Api::ok([
|
||||
'people' => array_values($rows),
|
||||
// 标签云(筛选条用)+ 标签词表(编辑时选已有标签)
|
||||
@@ -1742,6 +1763,58 @@ final class ApiHandler
|
||||
{
|
||||
[$fam, $pid] = Api::me();
|
||||
|
||||
/* 🔴 v62(用户 2026-10-19):「设置里面的影院管理,这些功能都还没有,
|
||||
应该补充,就是增加影院呢、修改影院呢、**高级搜索、统计、年度报告**,
|
||||
这些全部都应该补充进去。」
|
||||
⇒ 网页端早就有 `movies/search` / `movies/stats` / `movies/report`
|
||||
(见 `MovieStats`),但 **API 一个都没暴露** —— 端上只能干看着。
|
||||
这里按网页同一套 `MovieStats` 暴露成子路由,口径与网页**完全一致**
|
||||
(统计数字两边必须一样,否则用户会发现"网页 124 部、App 121 部")。
|
||||
⚠️ 这三个都是 **家族级**(不针对某一条观影记录)⇒ 必须在 `$id > 0`
|
||||
分支**之前**处理,否则 `/movies/stats` 会被当成"id=0 的详情"。 */
|
||||
if ($sub === 'stats' && $id === 0) {
|
||||
if ($method !== 'GET') { Api::fail('不支持的方法。', 'method_not_allowed', 405); }
|
||||
Api::ok([
|
||||
'overview' => MovieStats::overview($fam),
|
||||
'by_cinema'=> MovieStats::byCinema($fam),
|
||||
'by_person'=> MovieStats::byPerson($fam, $pid),
|
||||
'by_month' => MovieStats::byMonth($fam, 12),
|
||||
'by_year' => MovieStats::byYear($fam),
|
||||
]);
|
||||
}
|
||||
if ($sub === 'report' && $id === 0) {
|
||||
if ($method !== 'GET') { Api::fail('不支持的方法。', 'method_not_allowed', 405); }
|
||||
$years = MovieStats::years($fam);
|
||||
$year = (int)Api::int('year');
|
||||
// 没给年份 / 给的年份没有数据 ⇒ 退回最新有数据的那一年(与网页同一规则)
|
||||
if ($year <= 0 || !in_array($year, array_map('intval', $years), true)) {
|
||||
$year = count($years) > 0 ? (int)$years[0] : (int)date('Y');
|
||||
}
|
||||
Api::ok([
|
||||
'year' => $year,
|
||||
'years' => count($years) > 0 ? array_map('intval', $years) : [$year],
|
||||
'stats' => MovieStats::annual($fam, $pid, $year),
|
||||
]);
|
||||
}
|
||||
if ($sub === 'search' && $id === 0) {
|
||||
if ($method !== 'GET') { Api::fail('不支持的方法。', 'method_not_allowed', 405); }
|
||||
$p = [
|
||||
'title' => Api::str('title'),
|
||||
'cinema_id' => Api::int('cinema_id'),
|
||||
'person_id' => Api::int('person_id'),
|
||||
'date_from' => (string)(Api::param('date_from') ?? ''),
|
||||
'date_to' => (string)(Api::param('date_to') ?? ''),
|
||||
];
|
||||
$has = false;
|
||||
foreach ($p as $v) { if ($v !== '' && $v !== 0) { $has = true; break; } }
|
||||
// ⚠️ 条件全空时**不查全库**(那等于"列出所有观影",不是搜索)——
|
||||
// 与网页 `hasCond` 同一行为,端上据此提示"先设条件"。
|
||||
Api::ok([
|
||||
'has_cond' => $has ? 1 : 0,
|
||||
'rows' => $has ? MovieStats::search($fam, $p, $pid) : [],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($id > 0) {
|
||||
$m = Movie::find($fam, $id, $pid);
|
||||
if (!$m) { Api::notFound('这条观影记录'); }
|
||||
|
||||
Reference in New Issue
Block a user