本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/community_bank/说明.md。
社区银行(community_bank)插件说明
版本:1.0.0 | 作者:FlintHub | 状态:✅ 已启用
功能:把闲置的核心积分存入社区银行,按分段日息计息、随时存取;
利息只记在银行余额里,取款时才动核心积分(减少跨库写)。
定位:社区银行只负责积分 —— 进出全部是核心积分,银行余额只是「积分的一种存储形态」,
与支付、充值、提现无关。
一、功能简介
1.1 前台
| 页面 | 路径 | 说明 |
|---|---|---|
| 社区银行 | /bank | 未开户显示开户卡片;已开户显示余额、当前日息、存取款表单、利率表、最近流水 |
| 我的流水 | /bank/log | 分页列表(每页 LOG_PER_PAGE = 30 条),含时间/类型/金额/余额/备注 |
| 存款排行 | /bank/rank | 公开页,取余额 > 0 的前 RANK_LIMIT = 20 名,前三名有奖牌样式,本人行高亮 |
存取款为同步表单提交(PRG 模式):POST 处理完 → redirect('/bank'),
避免刷新重复提交。所有写操作走核心 Csrf::verifyOrDie()。
1.2 后台(/admin/community-bank)
单页五分区,靠页内 .admin-tabs 互跳(?tab=overview|rates|accounts|log|settings):
| 分区 | 参数 | 说明 |
|---|---|---|
| 总览 | ?tab=overview | 统计卡(账户数 / 存款总额 / 累计存入 / 累计利息 / 冻结数)+ 对账表 |
| 利率 | ?tab=rates | 分段利率档位增删改(min / max / 日利率 %),全量替换保存 |
| 账户 | ?tab=accounts | 账户列表 + 冻结/解冻 + <details> 内嵌手工调账(±余额,只动银行余额) |
| 流水 | ?tab=log | 全站流水(每页 ADMIN_PER_PAGE = 50 条) |
| 设置 | ?tab=settings | 总开关 / 开户费 / 单次最少存入 / 单户余额上限 |
⚠️ 后台所有 markup 必须留在
<main class="admin-content">内 —— 核心表单/按钮/alert 样式全是
.admin-content作用域(admin.css:237-306),放到</main>之后就是「弹出页没有 UI」且零日志。
二、目录结构
plugins/community_bank/
├── plugin.json # 插件清单(5 个钩子 + admin_url=/admin/community-bank)
├── Plugin.php # 主类:建表/迁移、设置、利率、账户、存取、惰性计息、对账、排行
├── FrontController.php # 前台:/bank 首页 / 开户 / 存取 / 流水 / 排行
├── AdminController.php # 后台:五分区 + 4 个写操作(继承 Admin\BaseController)
├── hook/
│ ├── init_after.php # 幂等建表兜底(版本化 marker 门控)
│ ├── route_register.php # 前台路由(6 条)
│ ├── admin_route_register.php # 后台路由(5 条,不写 /admin 前缀)
│ ├── nav_plugin_links.php # 「应用」入口(左栏 + 顶栏,双触发)
│ └── route_after_dispatch.php # ★ 惰性计息(每请求触发 → 零查询短路节流)
├── views/ # 4 个视图:index.php / log.php / rank.php / admin.php
├── assets/ # style.css(前台)/ admin.css(后台)/ script.js(分区切换 + 利率增删行)
├── lang/ # zh / en / zh_tw(各 140 键,键集合与键序零差异)
└── data/ # 独立库 + 建表 marker(自动生成 .htaccess / web.config 防 Web 访问)三、数据表(插件独立库)
库文件:plugins/community_bank/data/community_bank.sqlite
| 表 | 用途 | 关键字段 |
|---|---|---|
cb_accounts | 账户主表(也是余额台账) | user_id(PK) / balance / total_deposited / last_accrual_at / opened_at / status |
cb_transactions | 银行余额变动流水 | user_id / type / amount(±) / balance_after / memo / created_at |
cb_rates | 分段利率档位 | min_amount / max_amount(-1=上不封顶) / daily_bp(万分之一) / sort_order |
cb_settings | 插件设置 KV | k(PK) / v |
索引(activate() 内、CREATE TABLE 之后建):
idx_cbt_user (user_id, id DESC) / idx_cbt_all (id DESC) / idx_cba_balance (balance DESC)
账户状态:1 正常 | 0 冻结(status <> 1 一律拒绝存取与计息)
流水类型(type 列 → i18n txn_*):
| type | 含义 | amount 符号 |
|---|---|---|
deposit | 存入 | + |
withdraw | 取出 | − |
withdraw_rollback | 取出回滚(补偿) | + |
interest | 利息 | + |
adjust | 管理员调账 | ± |
⚠️ 金额一律以整数积分存储,杜绝浮点误差。
⚠️ 开户费不写这张表 —— 理由见 §5.4。
默认利率档位(站长拍板口径,seedDefaults() 仅在 cb_rates 空表时写入):
| 存款区间 | 日利率 | bp |
|---|---|---|
0 ~ 999 | 0.08% | 8 |
1000 ~ 4999 | 0.03% | 3 |
≥ 5000 | 0.01% | 1 |
默认设置(Plugin::defaults()):enabled=1 / open_fee=0 / min_deposit=1 / max_balance=0(不限)
四、路由清单
4.1 前台(hook/route_register.php)
| 方法 | 路径 | 鉴权 |
|---|---|---|
| GET | /bank | 登录 |
| POST | /bank/open | 登录 → CSRF |
| POST | /bank/deposit | 登录 → CSRF |
| POST | /bank/withdraw | 登录 → CSRF |
| GET | /bank/log | 登录 |
| GET | /bank/rank | 公开(无需登录) |
4.2 后台(hook/admin_route_register.php,不写 /admin 前缀)
⚠️ 核心已在 admin 分组内注册,插件再写
/admin会 404(规范 §11.22)。
plugin.json的admin_url = /admin/community-bank(含前缀),两边拼起来才是最终 URL。
| 方法 | 路径(不含前缀) | 动作 |
|---|---|---|
| GET | /community-bank | 五分区首页(?tab= 切换) |
| POST | /community-bank/rates/save | 利率档位全量保存 |
| POST | /community-bank/settings/save | 设置保存 |
| POST | /community-bank/account/status | 冻结 / 解冻 |
| POST | /community-bank/account/adjust | 手工调账(±) |
五、核心规则(改这个插件前必读)
5.1 ★ 跨库无事务 → 顺序固定 + 补偿
银行余额在插件库(community_bank.sqlite)、核心积分在核心库(business.sqlite),
两个独立 SQLite 文件,无法同事务。因此:
- 存入(deposit):① 先
Points::deduct()扣核心积分 → ② 后加银行余额;
②失败 → Points::award() 补偿退回。
- 取出(withdraw):① 先条件 UPDATE 扣银行余额(
balance >= :a原子防透支,rowCount()===0即拒绝)
→ ② 后 Points::award() 加核心积分;②失败 → 补偿把余额加回,并记一条 withdraw_rollback 流水。
⚠️ 顺序不是随便定的:先扣的一方是「可拒绝」的(积分不足 / 余额不足都能提前返回),
后写的一方才是需要补偿的。反过来会先动银行余额却卡在积分不足上,补偿面更大。
5.2 ★ 惰性计息(本插件的核心算法)
核心 cron_trigger.php 没有任何插件钩子,插件也不能往核心队列塞自定义任务类型
(规范 §11.4 #26)→ 利息结算只能走「惰性」:用户访问时按距上次结算的整日数补算。
入口 = hook/route_after_dispatch.php(每页触发),三重防线:
- 零查询短路:
$_SESSION['cb_last_tick']节流(ACCRUAL_THROTTLE_SEC= 600 秒)→ 窗口内直接 return - 先占坑:
$_SESSION['cb_last_tick'] = now(即使后续结算异常也不在窗口内反复重试) - 查询预算闸门:
Plugin::budget(5)(本插件自实现,核心无此方法)
算法(Plugin::accrue()):
days = floor((now - last_accrual_at) / 86400) ← ★ 按整日
days <= 0 → 返回 0,★ 完全不动 last_accrual_at(保留不足 1 天的零头)
days >= 1 → last_accrual_at += days * 86400 ← ★ 按整日推进,不是设为 now
interest = floor(balance * daily_bp * days / 10000)⚠️ **
last_accrual_at必须按整日推进(last += days*86400),绝不能= now** ——否则每次结算都会把「不足 1 天的零头」吃掉,用户频繁访问 = 利息永远攒不满一天。
⚠️ 利率档位按结算时的余额取(
rateBpFor($balance)),余额跨档后从下一整日起按新档计。⚠️ 利息只写银行余额(
cb_accounts.balance),不调Points::award()→ 无跨库写。
5.3 ★ 利息只记银行余额,取款才动核心积分
这是「减少跨库写」的设计取向:日常计息完全在插件库内完成,不碰核心库;
只有当用户主动取款时,才发生一次「银行余额 → 核心积分」的跨库写入。
5.4 ★ 开户费不写银行流水
openAccount() 的开户费从核心积分扣(Points::deduct),不写 cb_transactions。
理由:该表的语义是「银行余额变动记录」,而开户费只动核心积分、不动银行余额 ——
若记进去,reconcile()(Σbalance 应等于 Σ流水 amount)会永远差一个 +fee,对账口径就废了。
开户费的去向在核心积分明细(points_log,related_type='community_bank')里已有完整记录,信息不丢。
5.5 对账(Plugin::reconcile())
Σ(cb_accounts.balance) 应等于 Σ(cb_transactions.amount) ← diff 应为 0后台「总览」分区直接展示 account_sum / txn_sum / diff。任何 diff ≠ 0 都说明
有「改了余额却没记流水」或反之的写入路径 —— 新增写余额的代码必须同时 logTxn()。
5.6 设置保存:字段缺失即跳过
saveSettings() 的调用方(AdminController::settingsSave())必须用 array_key_exists 判断
「字段是否提交」,缺失即跳过 —— 否则 (int)($_POST[$k] ?? 0) 会把未提交的字段静默写成最小值
(302 + 提示成功,但 DB 与缓存都不变、error.log 也无记录)。这是规范 §11.32 的复发坑。
六、注意事项(踩过的坑)
- 目录名含下划线 →
Plugin.php末尾必须class_alias—— 核心按目录名硬拼类名
Plugin\community_bank\Plugin,与命名空间 Plugin\CommunityBank 不等(下划线不是大小写差异)
→ 生命周期三方法被静默跳过 → 卸载留孤儿表。已用 class_alias(..., false) 兜底。
- 命名空间首段必须与目录名逐字节一致(或靠 alias) —— 否则 Linux 上自动加载器 3 条候选路径
全落空 → Class not found → 500。Windows 大小写不敏感 = 本机假通过,必须跑
.workbuddy-ai/tools/plugin_case_lint.py。
nav_plugin_links双触发 —— 该钩子文件禁声明顶层函数/类,否则降级为include_once,
第二处入口静默消失。
route_after_dispatch必须节流 —— 该钩子每请求触发,未加节流 = 每请求一次全表扫描。
本插件用 $_SESSION 节流 + 先占坑 + 查询预算三重防护。
- 建表 marker 带版本号(
data/.schema.v1.ok)—— 表结构变更时必须递增文件名,
否则老安装不会重跑 activate()。每个入口另有 ensureSchema() 兜底。
- 索引 DDL 必须写在
CREATE TABLE之后,且用CREATE INDEX IF NOT EXISTS。 - **
--mn-danger/--mn-radius-*核心未定义** —— 插件 CSS 只用核心真实存在的--mn-*变量;
核心也没有 .mn-alert-warning(只有 .mn-alert/-success/-error)→ 本插件自定义 .cb-alert-warn,
复用核心的 --mn-comp-alert-warning-{bg,border,text} 三件套。
.mn-section自身无 padding —— 靠.mn-section-header撑开;插件以.mn-section作根容器时
必须自补内边距(.cb-wrap)。
- 核心
.admin-table按列序硬编码(td:nth-child(4)限宽、移动端按「第1列=ID、第2列=标题」重排)
→ 列序语义不同的插件表格自写类名,不用 .admin-table。
- 核心布局无 js section —— 脚本只能在
content末尾以<script src>引入。 pagination()返回值已自带转义(TemplateCompiler:557-563)→ 直接 echo,不可再$this->e()。- 发布前必跑:
python .workbuddy-ai/tools/plugin_case_lint.py plugins/community_bank.workbuddy-ai/tools/php_var_cjk_check.php plugins/community_bank- 零内联扫描 + 三语键集合比对(
.workbuddy-ai/tmp/cb_lang_check.php)
- ★ 新插件上线后必须清一次页面缓存 —— 核心
PageCache缓存游客的列表页/首页整页 HTML,
插件入口的钩子输出会被写进旧缓存 → 症状是「游客看不到入口、登录用户能看到」。
用 PageCache::invalidate()(不是 clear())清空并回读校验(内部用 @unlink 会静默失败)。
- ★★ 积分
reason必须传「给人看的文案」,不是机器标识 —— 核心
Notification::notifyPointsDeduct() / notifyPointsGain() 会把 Points::*() 的第 3 参 $reason
原样拼进站内通知标题(你因 {reason} 扣除 N 积分 / 你通过 {reason} 获得 N 积分),
并同时写进 points_log.reason 与 notifications.summary。
传 community_bank:deposit 这类内部代号,用户就会在通知里看到它(2026-10-01 站长实测反馈)。
机器标识走第 5 参 related_type(= Plugin::RELATED_TYPE),两者职责不同、不可混用。
本插件的文案统一经 Plugin::reason($key) 取;常量表里必须写完整键名字面量,
否则静态 i18n 扫描器会判成「语言包冗余 + 源码引用了不存在的键」。
⚠️ 探针清库时必须一起清 notifications —— 只清 points_log 会留下孤儿通知,
而 SQLite 会复用已删除用户的 id → 孤儿通知会「复活」到新用户头上。
七、上线前检查清单
- [ ] 后台「设置」分区确认总开关 = 开
- [ ] 后台「利率」分区确认三档利率符合预期(改完会全量替换)
- [ ] 确认开户费 / 单次最少存入 / 单户余额上限符合预期(0 = 开户免费 / 上限不限)
- [ ] 前台
/bank开户 → 存入 → 取款 → 流水 全链路走一遍 - [ ] 后台「总览」对账表
diff为 0 - [ ] 部署后执行一次后台「维护 → 清理缓存」(清 PageCache,否则游客看不到入口)
- [ ] 新增语言键已同步三语字典(本版 140 键,键集合与键序零差异)
八、当前状态
- ✅ 插件骨架 / 数据层 / 前台 3 视图 / 后台五分区 / 5 个钩子 / 三语语言包
- ✅ 静态校验:
php -l15 文件零错误 / 命名空间 Linux 精确匹配 / 零内联 /
三语各 140 键集合零差异(含键序一致) / i18n 引用键零缺失 / 全角标点零命中
- ✅ 静态契约探针 33 项全绿 —— 类名/
class_alias/ 钩子名合法性 / 视图零内联 /
$this->method() 存在性 / CSS 变量未裸用 / 路由前缀契约 / 后台 markup 作用域
- ✅ CLI 业务逻辑探针 80 项全绿 —— 档位命中与格式化 / 开户(含开户费扣减与补偿)/
存款跨库 + 补偿 / 取款跨库 + 防透支 + 补偿 / 惰性计息时间旅行
(整 30 天 + 5 秒零头:last_accrual_at 按整日推进且零头未被吃掉)/ 后台调账 /
对账 diff=0 / 冻结拒写 / 不存在账户 / 流水与排行 / 余额上限
- ✅ HTTP 端到端冒烟 59 项全绿 —— 前台路由 200/302、后台三重门禁
(未登录 / 非管理员 / 未二次验证)、五分区渲染、静态资源可达、
数据防护文件(.htaccess / web.config)、CSRF 保护、i18n 无泄漏键、
导航入口存在、库表增量守卫(跑完零残留)
- ✅ 已修复 1 个真实产品缺陷 ——
reconcile()口径被开户费流水破坏(见 §5.4) - ✅ 已修复 1 个真实环境问题 —— 游客首页
PageCache陈旧导致看不到/bank入口
(已用 PageCache::invalidate() 清空并回读校验)
- ⏳ 转账(扣 1% 积分手续费)与定期存款属 P3,本次未做
- ⏳ 贷款功能按决策不做