- New fastadmin/chathub/ (11 files, 204K): user-facing FastAdmin ThinkPHP 5 addon
- _markOrderPaid() now calls _provisionAsync() on empty embed_code (closes 'paid but no code' gap)
- New reprovision() action — user-initiated resource rebuild
- payReturn() smart redirect: 3 branches (just_paid / provisioning / pending / fallback)
- status badge updated with 'provisioning' state (blue)
- _initialize() whitelist expanded: reprovision (user) + payNotify/payReturn (public webhook)
- 5 chathub_* tables (tenant/log/order/channel_account/gateway_log) + MIGRATIONS.md
Bugfixes during E2E:
- payNotify HTTP 500: tenant.status ENUM missing 'provisioning' value (DBA migration)
- payNotify HTTP 500: chathub_log.status='received' (not in ENUM) — changed to 'success'
- TP5 method signature: function reprovision(\$ids) does not read query string — use \$this->request->param('ids')
8.0 KiB
FastAdmin ChatHub Addon
PHP frontend for the Chatwoot AI multi-tenant SaaS system, packaged as a FastAdmin addon (ThinkPHP 5).
What this is
A self-contained FastAdmin plugin that provides the user-facing side of the platform: registration, plan selection, payment (Alipay/WeChat), member center, and channel credential management. All HTML is inlined in the controller (no view/ templates, no FastAdmin render engine) — see "Architecture" below for why.
The service-side lives in this same monorepo, one level up: gateway/ (in-process Python library), chatwoot_ws_agent.py (WebSocket agent), and provision_server.py (HTTP provisioning API). The addon talks to those via HTTP only.
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ FastAdmin ChatHub Addon (this dir) │
│ │
│ public/ → register / landing / my / channelAuth / payNotify │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ controller/Index.php (2108 lines) │ │
│ │ • All HTML inlined as PHP heredoc │ │
│ │ • POST to chathub-provision over HTTP/JSON │ │
│ │ • _initialize() whitelists public + user actions │ │
│ │ • Idempotency-Key for safe webhook retries │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ model/ChathubTenant.php → TP5 model with JSON getters │
│ config.php → 17 admin-fillable config fields │
│ install.sql → 5 tables (tenant/log/order/...) │
└──────────────────────────────────┬───────────────────────────────┘
│ HTTP/JSON
▼
┌──────────────────────────────────────┐
│ chathub-provision (Python service) │
│ :5566 /provision /suspend ... │
└──────────────────────────────────────┘
Why no view/ directory?
FastAdmin's template engine uses ThinkPHP's view() / fetch() with ThinkTemplate syntax. For this addon, we deliberately use pure echo + PHP heredoc strings inside the controller because:
- Single-file deploys — the entire UI for
my()andregister()is incontroller/Index.php, no template files to keep in sync. - Dynamic flash messages — the
FLASH_MESSAGEplaceholder is replaced viastr_replaceat the end ofmy()based on query string params (?just_paid=1/?provisioning=1/?pending=1/?error=...). - No template-engine dependency — works on any ThinkPHP 5 install without
think-templatepackage.
If you need to customize the UI, edit the heredoc strings inside controller/Index.php directly. Search for register() and my() for the two main render blocks.
Routes (URL → controller action)
| URL | Action | Auth | Purpose |
|---|---|---|---|
/addons/chathub/index/landing |
landing |
public | Landing page |
/addons/chathub/index/register |
register |
public | Registration + plan selection |
/addons/chathub/index/login |
login |
public | Login form |
/addons/chathub/index/doLogin |
doLogin |
public | Login submit |
/addons/chathub/index/logout |
logout |
public | Logout |
/addons/chathub/index/my |
my |
user (session) | Member center (tenant list) |
/addons/chathub/index/channelList |
channelList |
user | Choose a channel to bind |
/addons/chathub/index/channelAuth |
channelAuth |
user | OAuth redirect (5 platforms) |
/addons/chathub/index/channelCallback |
channelCallback |
user | OAuth callback |
/addons/chathub/index/reprovision |
reprovision |
user | Manually retry provisioning |
/addons/chathub/index/payAlipay |
payAlipay |
user | Start Alipay payment |
/addons/chathub/index/payWechat |
payWechat |
user | Start WeChat payment |
/addons/chathub/index/payNotify |
payNotify |
public (webhook) | Alipay/WeChat async callback |
/addons/chathub/index/payReturn |
payReturn |
public (webhook) | Alipay/WeChat sync return |
Install
1. Copy addon directory
cp -r fastadmin/chathub /www/sites/<your-site>/index/addons/
2. Run SQL
The 5 tables are defined in install.sql (uses __PREFIX__ placeholder for FastAdmin table prefix). Run via phpMyAdmin or mysql:
mysql -u <user> -p <dbname> < install.sql
The script will substitute __PREFIX__ with fa_ (or whatever your prefix config is) — FastAdmin's db()->execute() does this automatically, or you can sed first.
If you already have a v1.0–v1.5 install, run MIGRATIONS.md instead.
3. Enable the addon
FastAdmin Admin → 插件管理 → local install → upload this dir → enable.
4. Configure
FastAdmin Admin → 插件管理 → ChatHub → 配置:
| Field | Example | Required |
|---|---|---|
provision_server_url |
http://CoPaw:5566 |
yes |
site_name |
ChatHub |
yes |
chatwoot_url |
https://chatwoot.example.com |
yes |
chatwoot_api_token |
(your personal access token) | yes |
alipay_app_id / alipay_merchant_private_key / alipay_public_key |
(from 支付宝开放平台) | if 支付宝 enabled |
wechat_app_id / wechat_mch_id / wechat_api_v3_key / wechat_cert_path / wechat_key_path |
(from 微信支付商户平台) | if 微信支付 enabled |
Important: All payment fields use sandbox values by default. Switch off alipay_sandbox / wechat_sandbox for production.
5. Verify
curl -I https://<your-site>/addons/chathub/index/landing
# should be HTTP 200
Tenant lifecycle
register() → status='provisioning' (called from chathub-provision sync)
│
├─ success → status='active' (embed_code populated)
│
└─ failure → status='pending' (user can retry from my.html)
payNotify() → _markOrderPaid() + _provisionAsync() if embed_code empty
│
└─ _provisionAsync() success → status='active' + embed_code written
reprovision() → user-initiated retry; same path as payNotify fallback
The provisioning state is new in v1.6 (see MIGRATIONS.md). Before that, the state machine went straight pending → active, which meant a payment that succeeded but the chathub-provision call timed out left the user in a "paid but no embed code" limbo.
Changelog (this component)
- v1.8 — Payment flow completion:
_markOrderPaidcalls_provisionAsyncon emptyembed_code; newreprovisionaction;payReturnsmart redirect (3 branches);provisioningstate badge; schema migration - v1.6 — Channel bindings (
channelAuth/channelCallback) for Amazon/JD/Taobao/PDD/TikTok - v1.0 — Initial release: register/login/my/landing
License
AGPL v3 — see LICENSE and root LICENSE.
Related
../gateway/— Platform Gateway Python library (5 platform adapters)../chatwoot_ws_agent.py— WebSocket agent that calls Gateway../provision_server.py— HTTP provisioning API that the addon calls../CHANGELOG.md— Top-level changelog