配合端上「界面六条」,服务端只补三样数据:
① Family::slogan(fam, pid) —— 三层优先级
用户:「如果这个用户自己有标语,他可以写一句自己的标语;如果他有自己的标语,
这个地方就是用户自己自定义的标语。如果他没有标语,就用我们给他的这个标语。」
优先级:persons.slogan → families.motto → Family::DEFAULT_SLOGAN。
DEFAULT_SLOGAN = '一家人用岁月写成的一本书'(用户明确指定,别再改)。
与网页端 templates/home.php 的 heroSlogan 同一套规则,两边永远一致。
/login 与 /me 都返回 slogan —— 每次启动都会打 /me,所以网页端改了 slogan
手机下次启动就跟上,不必为此另加接口。
② /home 新增 recent_days —— 首页「最近纪念日」
用户:「未来最近的三个纪念日,包括每个人的生日,以及手动填写的各个纪念日。」
= Anniversary::upcoming(fam, pid, 370) 取前 3。
· 为什么是 370 天而不是网页端概述卡那样的 30 天:只取 30 天时,
一个"刚过完生日"的家庭会**一个都取不到**,模块直接空着 ——
而用户要的是一个永远的倒计时。取满一年保证"接下来的三个"一定有。
· 数据源 all() = 派生生日 + 自建纪念日(正好覆盖用户说的两样,
不必再去 Birthday:: 那套单独取一遍,否则迟早口径不一)。
· 每条带 days(倒计天数)、source(custom / birthday_solar / birthday_lunar),
顺序由服务端排好(最近的在前),端上不再排一遍。
③ 真实头像
用户:「右上角的头像应该是真实的。」
· /home 新增 me_avatar(当前登录者的 avatar_url());
· /login 的 person 增加 avatar_url。
avatar_url() 负责按线上存储驱动决定走 /uploads 还是 OSS 直链 ——
端上不自己拼路径(拼错了就是永远破图)。
验证:_fb_home_api.py 60/0(新增 16 条:recent_days 真数据/天数/排序/≤3 条、
me_avatar、slogan 三层优先级实测 —— 给 persons#1 写一句个人 slogan,
必须优先返回他那句);php -l 通过;部署脚本增「第59批形态」9 条。
真机实测:抽屉显示「修身,齐家」= 杨永全自己的 slogan(个人优先生效)。
101 lines
4.0 KiB
PHP
101 lines
4.0 KiB
PHP
<?php
|
||
/**
|
||
* 家庭:数据空间的根,所有名册条目都挂在它下面。
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App;
|
||
|
||
final class Family
|
||
{
|
||
/**
|
||
* 应用自己的口号(用户 2026-10-18 明确指定,别再改)。
|
||
*
|
||
* 🔴 用户原话:「我们应用的口号或者标语是:**一家人用岁月写成的一本书**。
|
||
* 这也是我们"家书"这个名字的由来。所以你不要再乱换这个标语了,
|
||
* 只要是想出现标语或者副标题的地方,就用这样一句话。」
|
||
*
|
||
* ⚠️ 它与 `families.motto`(用户可在网页端「系统设置」里自己写一句)
|
||
* 是**两件事**:这里这句是"谁都没写时的默认值",
|
||
* 用户写了自己的就用他自己的 —— 规则见 `slogan()`。
|
||
*/
|
||
public const DEFAULT_SLOGAN = '一家人用岁月写成的一本书';
|
||
|
||
/**
|
||
* 「我」这一端该显示的那句话(标语 / 副标题)。
|
||
*
|
||
* 优先级(与网页端 `templates/home.php` 的 `heroSlogan` **同一套规则**):
|
||
* ① **我自己的** `persons.slogan`(在「我的资料」里填的那一句);
|
||
* ② 家庭的 `families.motto`(网页端「系统设置」里那句,用户可自定义);
|
||
* ③ 应用默认口号 `DEFAULT_SLOGAN`。
|
||
*
|
||
* 🔴 为什么不是"简单取 motto":用户 2026-10-18 说得很清楚 ——
|
||
* 「如果这个用户自己有标语,他可以写一句自己的标语;……如果他没有标语,
|
||
* 就用我们给他的这个标语。」
|
||
* 个人 → 家庭 → 默认,正是这三层;网页端首页的 `heroSlogan` 已经是这个逻辑,
|
||
* 端上照抄同一套,两边才不会出现"网页一句、手机另一句"。
|
||
*
|
||
* @param int $familyId 家庭 id
|
||
* @param int|null $personId 当前登录者(null = 不取个人那句,直接走家庭/默认)
|
||
* @return string 永远非空(兜底是 DEFAULT_SLOGAN)
|
||
*/
|
||
public static function slogan(int $familyId, ?int $personId = null): string
|
||
{
|
||
if ($personId !== null && $personId > 0) {
|
||
try {
|
||
$st = Database::get()->prepare('SELECT slogan FROM persons WHERE id = ? AND family_id = ?');
|
||
$st->execute([$personId, $familyId]);
|
||
$mine = trim((string)($st->fetchColumn() ?: ''));
|
||
if ($mine !== '') {
|
||
return $mine;
|
||
}
|
||
} catch (\Throwable $ex) {
|
||
// 老库还没这一列 / 读不到都不该挡路:往下走家庭那句
|
||
}
|
||
}
|
||
|
||
try {
|
||
$f = self::find($familyId);
|
||
$motto = trim((string)($f['motto'] ?? ''));
|
||
if ($motto !== '') {
|
||
return $motto;
|
||
}
|
||
} catch (\Throwable $ex) {
|
||
// 同上:读不到就用默认
|
||
}
|
||
|
||
return self::DEFAULT_SLOGAN;
|
||
}
|
||
|
||
public static function find(int $id): ?array
|
||
{
|
||
$st = Database::get()->prepare('SELECT * FROM families WHERE id = ?');
|
||
$st->execute([$id]);
|
||
$row = $st->fetch();
|
||
return $row ?: null;
|
||
}
|
||
|
||
public static function create(string $name, string $motto = ''): int
|
||
{
|
||
$st = Database::get()->prepare('INSERT INTO families (name, motto) VALUES (?, ?)');
|
||
$st->execute([$name, $motto !== '' ? $motto : null]);
|
||
return (int)Database::get()->lastInsertId();
|
||
}
|
||
|
||
public static function update(int $id, string $name, string $motto = ''): void
|
||
{
|
||
$st = Database::get()->prepare('UPDATE families SET name = ?, motto = ? WHERE id = ?');
|
||
$st->execute([$name, $motto !== '' ? $motto : null, $id]);
|
||
}
|
||
|
||
public static function countMembers(int $familyId): int
|
||
{
|
||
$st = Database::get()->prepare(
|
||
"SELECT COUNT(*) FROM persons WHERE family_id = ? AND kind = 'member'"
|
||
);
|
||
$st->execute([$familyId]);
|
||
return (int)$st->fetchColumn();
|
||
}
|
||
}
|