Files
FamilyBook/app/PersonQuery.php
T
laoyang 52cc1ecccb v24 第 9 批:名册九条(分组按钮/加入名册/生日月日/地址四级/千人性能/拖动修复/删除人员/返回名册/Semi DatePicker)
- #131 名册卡片加「分组」按钮(details+radio,显示当前分组,选中即改)
- #132 修复「从已有人里加入名册」无反应
- #133 生日只显示月和日(阳历 MM-DD / 农历中文名),PersonQuery::birthdaysOf 取代 birthdayOf
- #134 地址结构化为 国家/省/市/区+详细:regions 表(3429 行全国区划)+ Region.php +
  ?r=regions 级联接口 + 表单 .addr-grid + 老地址回填(Schema v16)
- #135 千人名册性能:.person content-visibility:auto + 名册页 ob_gzhandler
  (实测 服务端 1.15s/3.8MB → 0.17s/87KB;客户端导航→load 1.35s → 0.37s)
- #136 拖动排序改 Pointer Events(Chrome 下 HTML5 DnD 在 DOM 变动时断)
- #137 个人详情页底部「删除人员」
- #138 编辑资料页加「返回名册」
- #139 所有日期选择换 Semi Design DatePicker(渐进增强:原生四字段保 POST 契约,
  懒加载 vendor semi UMD,值回写原字段;工作经历 type=month 一并升级;宣纸主题对齐)

验证:e2e 785/0、batch9 37/0、upgrade 126/0、picker 28/0、mobile 688/0、interact 97/0、
batch_e 91/0、import 21/0、douban 22/0、drag 5/0、semi-integrate 28/0、semi-probe 17/0、
geom 5/5;部署后线上自检 16/16(含 4 个 semi 资源 200 + regions 路由)。
2026-10-04 08:56:40 +08:00

144 lines
5.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 「找个人」的统一查法(v24 用户 2026-10-03 第 2 条)。
*
* 用户原话:「所有人员检索的地方,输入汉字或者首字母,自动筛选,而不需要按回车。」
*
* 这里只负责**服务端那一半**:把一份人员行按查询词筛一遍。
* 前端「输入即筛选」那一半在 assets/js/filter.js(纯 DOM 显隐,不发请求)。
*
* 为什么要把这段抽出来(而不是各页各写一遍):
* · 名册(Person::roster)、家里已有的人(Person::all)、以及以后任何"找个人"的地方,
* 判据必须**完全一致** —— 否则同一句「yyq」在名册里能搜到、在挑人页搜不到,
* 用户只会觉得"搜索坏了",而不会想到是两处实现不一样。
* · 上半段那个「要不要给 SQL 加 LIKE」的判断很容易写错(本项目已经错过一次):
* 纯字母(yyq)与纯数字(手机号)**都不能**交给 SQL 的 LIKE ——
* 前者 SQL 里没法把汉字转拼音,后者手机号是密文存的,一 LIKE 结果就成空。
*
* 分工:
* · needsSql() / likeArgs() → SQL 侧的**粗筛**(只在能帮忙时才加条件)
* · refine() → PHP 侧的**精筛**(一律跑,结果才是最终答案)
* · decorate() → 把「解密后的手机号 / 生日 / 我怎么称呼他」补齐给列表用
*/
declare(strict_types=1);
namespace App;
final class PersonQuery
{
/**
* 这个查询词该不该让 SQL 帮忙粗筛?
*
* 只有**库里有明文可比**的查询词值得:中文姓名、邮箱里的字母+符号。
* ⚠️ 纯字母(yyq = 姓名首字母)与纯数字(手机号)一律交给 PHP 精筛:
* · SQL 里没有"汉字→拼音"的函数,对 yyq 做 LIKE 必然 0 行;
* · 手机号是 Crypto 加密后存的(phone_encrypted),明文数字 SQL 永远匹配不到。
* 这两类一旦加了 SQL 条件,精筛阶段就已经没有数据可筛了,
* 症状是"搜 yyq 一个人都没有" —— 看着像功能没做。
*/
public static function needsSql(string $q): bool
{
$q = trim($q);
if ($q === '') {
return false;
}
return (bool)preg_match('/[^A-Za-z0-9]/u', $q);
}
/** SQL 粗筛要绑的参数(name / email / nickname 三个 LIKE) */
public static function likeArgs(string $q): array
{
$like = '%' . trim($q) . '%';
return [$like, $like, $like];
}
/** SQL 粗筛的 WHERE 片段(只在 needsSql() 为真时调用) */
public const SQL_WHERE = ' AND (p.name LIKE ? OR p.email LIKE ? OR n.nickname LIKE ?)';
/**
* PHP 精筛:姓名 / 我怎么称呼他 / 库里的旧小名 / 邮箱 / 解密后的手机号 / 姓名首字母。
*
* ⚠️ 进来之前**必须**先给每行补上 phone_plain(见 phonePlain()),
* 否则手机号这一路永远匹配不上(Pinyin::matches 认的是 phone_plain)。
*/
public static function refine(array $rows, string $q): array
{
$q = trim($q);
if ($q === '') {
return $rows;
}
$out = [];
foreach ($rows as $row) {
if (Pinyin::matches($row, $q)) {
$out[] = $row;
}
}
return array_values($out);
}
/**
* 解密手机号(失败一律当"没填",绝不让一行坏数据把整页筛挂)。
* @return string 明文;没有/解不开返回 ''
*/
public static function phonePlain(array $row): string
{
$enc = (string)($row['phone_encrypted'] ?? '');
if ($enc === '') {
return '';
}
try {
return (string)Crypto::decrypt($enc);
} catch (\Throwable $ex) {
return '';
}
}
/**
* 给列表用的行补上派生字段。幂等(已有值就不覆盖)。
*
* @param int $viewerId 谁在看(决定"我怎么称呼他")
* @param int $meId 用来标"这是我"(0=不标)
*/
public static function decorate(array $rows, int $viewerId, int $meId = 0): array
{
$graph = null;
$people = null;
foreach ($rows as &$row) {
if (!array_key_exists('phone_plain', $row)) {
$row['phone_plain'] = self::phonePlain($row);
}
if (!isset($row['shown'])) {
$row['shown'] = Nickname::display($viewerId, $row) ?: (string)($row['name'] ?? '');
}
}
unset($row);
return $rows;
}
/**
* 生日展示串(用户 2026-10-04 第 3 条:**阴历和阳历都显示,只显示月和日,不需要年**)。
*
* 两种记法是同一个人的同一天,所以不像以前那样"二选一"——都列出来,
* 长辈看农历、年轻人看阳历,各自认自己的那个。
*
* 农历不写 '02-03' 这种数字:**写成「二月初三」**,否则"农历 02-03"看着就像
* 阳历的 2 月 3 号,反而比不显示更误导(闰月还要带「闰」字)。
*
* @return array<int,array{0:string,1:string}> [['阳历','03-07'], ['农历','二月初三']](没有的就不出现)
*/
public static function birthdaysOf(array $row): array
{
$out = [];
$solar = Birthday::dateOnly((string)Birthday::solarOf($row));
if (preg_match('/^\d{4}-(\d{2})-(\d{2})$/', $solar, $m) === 1) {
$out[] = ['阳历', $m[1] . '-' . $m[2]];
}
$lun = Birthday::dateOnly((string)($row['birthday_lunar'] ?? ''));
if (preg_match('/^\d{4}-(\d{2})-(\d{2})$/', $lun, $m) === 1) {
$leap = !empty($row['birthday_lunar_leap']);
$out[] = ['农历', Lunar::monthName((int)$m[1], $leap) . Lunar::dayName((int)$m[2])];
}
return $out;
}
}