v1.8: FastAdmin chathub-addon — register/plan/payment/member-center + 5 channel bindings
- 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')
This commit is contained in:
@@ -1,5 +1,41 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.8 (2026-06-05) — FastAdmin 用户端 + 支付激活闭环
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
- **`fastadmin/chathub/`** — FastAdmin ThinkPHP 5 用户端插件 (11 文件 / 204K)
|
||||||
|
- **注册流程** — 选套餐 → 创建租户 → 调 chathub-provision 同步开通 → 写入 `embed_code`
|
||||||
|
- **支付激活闭环** — `_markOrderPaid()` 调 `_provisionAsync()` 补建资源, 避免 "付了款拿不到代码" 困境
|
||||||
|
- **`reprovision` action** — 用户主动重试 (登录后访问 `/addons/chathub/index/reprovision?ids={tenant_id}`)
|
||||||
|
- **`payReturn` smart redirect** — 3 分支: `?just_paid=1` (有资源) / `?provisioning=1` (付了款没资源) / `?pending=1` (订单未确认) / fallback `?order=X`
|
||||||
|
- **5 渠道绑定 UI** — `channelAuth` / `channelCallback` 完整 OAuth 流程 (Amazon/JD/Taobao/PDD/TikTok)
|
||||||
|
- **状态徽章** — 新增 `provisioning` 蓝 (#3b82f6), 完整状态机: pending→provisioning→active
|
||||||
|
- **`fa_chathub_order` 表** — 支付订单 + 续期
|
||||||
|
- **`fa_chathub_channel_account` 表** — AES-256-GCM 加密凭据 (5 平台)
|
||||||
|
- **`fa_chathub_gateway_log` 表** — Gateway 6 错误路径调用日志
|
||||||
|
- **`MIGRATIONS.md`** — v1.0 → v1.6 schema 升级脚本 (含 `provisioning` ENUM)
|
||||||
|
- **`_initialize()` 白名单** — `reprovision` (user) + `payNotify`/`payReturn` (public webhook) 加入 FastAdmin action 白名单
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- **`payNotify` HTTP 500** — 两层 ENUM schema 同步问题:
|
||||||
|
1. `fa_chathub_tenant.status` ENUM 加 `provisioning` (`ALTER TABLE ... MODIFY COLUMN ...`)
|
||||||
|
2. `fa_chathub_log.status` 写入了非法值 `'received'`, 改为 `'success'`
|
||||||
|
- **TP5 method signature bug** — `reprovision($ids = null)` 改 `$this->request->param('ids')` (TP5 不会自动注入 query string)
|
||||||
|
- **支付流程 E2E 验证** — 4 项改动 (`_markOrderPaid`+`_provisionAsync`, `reprovision`, `payReturn` smart redirect, `provisioning` 状态徽章) 全部通过 E2E
|
||||||
|
|
||||||
|
### 文件统计
|
||||||
|
- `controller/Index.php` 1964 → 2108 行 (+144, +1 fix)
|
||||||
|
- `install.sql` 2 张表 → 5 张表
|
||||||
|
- 配置文件 17 个后台可填字段
|
||||||
|
- `fastadmin/chathub/README.md` 完整安装/路由/状态机文档
|
||||||
|
|
||||||
|
### 部署注意
|
||||||
|
- 旧用户升级: 跑 `fastadmin/chathub/MIGRATIONS.md` 的 SQL
|
||||||
|
- 新用户安装: 用 `fastadmin/chathub/install.sql` (5 张表)
|
||||||
|
- FastAdmin 插件市场安装: 启用后到"插件管理 → ChatHub → 配置"填入 17 个字段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.7 (2026-06-05) — 对话上下文 + 客户画像 + 指数退避重连
|
## v1.7 (2026-06-05) — 对话上下文 + 客户画像 + 指数退避重连
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|||||||
@@ -227,9 +227,63 @@ chatwoot-ai-agent/
|
|||||||
├── requirements.txt # Python 依赖
|
├── requirements.txt # Python 依赖
|
||||||
├── chatwoot_auth.example.json # Session 认证文件模板
|
├── chatwoot_auth.example.json # Session 认证文件模板
|
||||||
├── inboxes.example.json # 路由配置模板
|
├── inboxes.example.json # 路由配置模板
|
||||||
|
├── fastadmin/ # FastAdmin 用户端 PHP 插件 (v1.8+)
|
||||||
|
│ └── chathub/ # 用户注册/付费/会员中心 + 渠道绑定 UI
|
||||||
|
│ ├── controller/Index.php # 2108 行 (HTML 内联, 14 个 action)
|
||||||
|
│ ├── model/ChathubTenant.php
|
||||||
|
│ ├── config.php # 17 个后台可填配置项
|
||||||
|
│ ├── install.sql # 5 张表 (tenant/log/order/channel_account/gateway_log)
|
||||||
|
│ ├── MIGRATIONS.md # v1.0 → v1.6 schema 升级脚本
|
||||||
|
│ ├── assets/css|js/ # 样式 + 表格初始化
|
||||||
|
│ └── README.md # 安装 + 路由表 + 生命周期
|
||||||
└── .gitignore
|
└── .gitignore
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## FastAdmin 用户端 (`fastadmin/chathub/`)
|
||||||
|
|
||||||
|
PHP 前端 (ThinkPHP 5 / FastAdmin addon),为整个系统提供用户面:注册 → 选套餐 → 支付 → 会员中心 → 渠道绑定。所有 HTML 都在 `controller/Index.php` 里以 PHP heredoc 形式内联,不依赖 FastAdmin 模板引擎。
|
||||||
|
|
||||||
|
### 路由表
|
||||||
|
|
||||||
|
| URL | Auth | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `/addons/chathub/index/landing` | public | 落地页 |
|
||||||
|
| `/addons/chathub/index/register` | public | 注册 + 选套餐 |
|
||||||
|
| `/addons/chathub/index/login` / `doLogin` / `logout` | public | 登录登出 |
|
||||||
|
| `/addons/chathub/index/my` | user (session) | 会员中心 (租户列表 + 嵌入代码 + 重新开通) |
|
||||||
|
| `/addons/chathub/index/channelList` / `channelAuth` / `channelCallback` | user | 5 平台渠道绑定 |
|
||||||
|
| `/addons/chathub/index/reprovision` | user | 用户主动补建资源 |
|
||||||
|
| `/addons/chathub/index/payAlipay` / `payWechat` | user | 发起支付 |
|
||||||
|
| `/addons/chathub/index/payNotify` | public (webhook) | 支付宝/微信异步回调 (验签 + 开户) |
|
||||||
|
| `/addons/chathub/index/payReturn` | public (webhook) | 支付宝/微信同步跳转 (3 分支 smart redirect) |
|
||||||
|
|
||||||
|
### 与 chathub-provision 的契约
|
||||||
|
|
||||||
|
`payNotify` 和 `register` 调用 `chathub-provision` 服务的 `/provision` 端点:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST {provision_server_url}/provision
|
||||||
|
Headers: X-API-Key, Idempotency-Key, Content-Type: application/json
|
||||||
|
Body: {"name": "...", "domain": "...", "email": "...", "type": "web_widget", "max_agents": 3}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Idempotency-Key` 用 `reprov-{tenant_id}-{md5(domain|channel_type)}` 计算,允许 PHP 重试时 chathub-provision 端去重 (5 分钟 TTL)。
|
||||||
|
|
||||||
|
### 状态机
|
||||||
|
|
||||||
|
```
|
||||||
|
register → status='provisioning' ─┬─ success → status='active' (embed_code 写入)
|
||||||
|
│
|
||||||
|
└─ failure → status='pending' (用户点"重新开通")
|
||||||
|
|
||||||
|
payNotify → _markOrderPaid() + _provisionAsync() (若 embed_code 为空)
|
||||||
|
└─ success → status='active'
|
||||||
|
|
||||||
|
reprovision → 用户主动重试
|
||||||
|
```
|
||||||
|
|
||||||
|
详见 `fastadmin/chathub/README.md`。
|
||||||
|
|
||||||
## 版本历史
|
## 版本历史
|
||||||
|
|
||||||
| 版本 | 说明 |
|
| 版本 | 说明 |
|
||||||
@@ -242,6 +296,7 @@ chatwoot-ai-agent/
|
|||||||
| v1.5 | 消息防抖(5s 累积合并),AI 错误重试(指数退避)|
|
| v1.5 | 消息防抖(5s 累积合并),AI 错误重试(指数退避)|
|
||||||
| v1.6 | Platform Gateway 库——Amazon/JD/Taobao/PDD/TikTok 5 平台统一 API 集成 |
|
| v1.6 | Platform Gateway 库——Amazon/JD/Taobao/PDD/TikTok 5 平台统一 API 集成 |
|
||||||
| v1.7 | 对话上下文(`--session-id`)+ 对话摘要 + 客户画像 + WebSocket 指数退避重连 |
|
| v1.7 | 对话上下文(`--session-id`)+ 对话摘要 + 客户画像 + WebSocket 指数退避重连 |
|
||||||
|
| v1.8 | FastAdmin 用户端 PHP 插件 (`fastadmin/chathub/`) ——注册/选套餐/支付/会员中心/5 渠道绑定, 支付完成后自动调 chathub-provision 补建资源, 修复 `payNotify` HTTP 500 (ENUM schema + log status bug), 新增 `reprovision` 用户主动重试 |
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
namespace addons\chathub;
|
||||||
|
|
||||||
|
use app\common\library\Menu;
|
||||||
|
use think\Addons;
|
||||||
|
|
||||||
|
class Chathub extends Addons
|
||||||
|
{
|
||||||
|
protected $menu = [
|
||||||
|
[
|
||||||
|
'name' => 'chathub',
|
||||||
|
'title' => 'ChatHub',
|
||||||
|
'icon' => 'fa fa-headset',
|
||||||
|
'ismenu' => 1,
|
||||||
|
'weigh' => 1,
|
||||||
|
'sublist' => [
|
||||||
|
["name" => "chathub/index/index", "title" => "租户列表"],
|
||||||
|
["name" => "chathub/index/dashboard", "title" => "控制面板"],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
public function install()
|
||||||
|
{
|
||||||
|
Menu::create($this->menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uninstall()
|
||||||
|
{
|
||||||
|
Menu::delete("chathub");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enable()
|
||||||
|
{
|
||||||
|
Menu::enable("chathub");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disable()
|
||||||
|
{
|
||||||
|
Menu::disable("chathub");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upgrade()
|
||||||
|
{
|
||||||
|
Menu::upgrade('chathub', $this->menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* config_init 钩子
|
||||||
|
*/
|
||||||
|
public function ConfigInit()
|
||||||
|
{
|
||||||
|
// nothing to init
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容 fallback
|
||||||
|
*/
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 GreatQiu
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Chathub FastAdmin Addon — Migrations
|
||||||
|
|
||||||
|
This file documents schema migrations applied to the chathub tables between releases. Always back up your database before applying migrations.
|
||||||
|
|
||||||
|
## v1.6 — channel_type + status enum extension
|
||||||
|
|
||||||
|
**Date:** 2026-06-05
|
||||||
|
**Reason:** Support 5 platform integrations (Amazon/京东/淘宝/拼多多/抖音) and add explicit "provisioning" state in tenant lifecycle.
|
||||||
|
|
||||||
|
### `fa_chathub_tenant`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE `fa_chathub_tenant`
|
||||||
|
ADD COLUMN `user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'fa_user.id (创建者)' AFTER `id`,
|
||||||
|
ADD COLUMN `team_id` INT(11) DEFAULT NULL COMMENT 'Chatwoot Team ID' AFTER `inbox_token`,
|
||||||
|
ADD COLUMN `max_agents` INT(11) NOT NULL DEFAULT 3 COMMENT '最大坐席数' AFTER `agent_name`,
|
||||||
|
ADD COLUMN `expire_at` DATETIME DEFAULT NULL COMMENT '到期时间' AFTER `provisioned_at`,
|
||||||
|
ADD KEY `idx_user_id` (`user_id`),
|
||||||
|
MODIFY COLUMN `channel_type` ENUM('web_widget','api','amazon','jd','taobao','pdd','tiktok') NOT NULL DEFAULT 'web_widget' COMMENT '通道类型',
|
||||||
|
MODIFY COLUMN `status` ENUM('pending','provisioning','active','suspended','disabled') NOT NULL DEFAULT 'pending' COMMENT '状态';
|
||||||
|
```
|
||||||
|
|
||||||
|
### `fa_chathub_log`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE `fa_chathub_log`
|
||||||
|
MODIFY COLUMN `tenant_id` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID (0=系统级)';
|
||||||
|
```
|
||||||
|
|
||||||
|
### New tables
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- v1.8 支付订单
|
||||||
|
CREATE TABLE IF NOT EXISTS `fa_chathub_order` (
|
||||||
|
... (see install.sql for full schema)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- v1.6 渠道账号 (5 平台)
|
||||||
|
CREATE TABLE IF NOT EXISTS `fa_chathub_channel_account` (
|
||||||
|
... (see install.sql for full schema)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- v1.6 Gateway 调用日志
|
||||||
|
CREATE TABLE IF NOT EXISTS `fa_chathub_gateway_log` (
|
||||||
|
... (see install.sql for full schema)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- v1.6 rollback
|
||||||
|
ALTER TABLE `fa_chathub_tenant`
|
||||||
|
DROP COLUMN `user_id`,
|
||||||
|
DROP COLUMN `team_id`,
|
||||||
|
DROP COLUMN `max_agents`,
|
||||||
|
DROP COLUMN `expire_at`,
|
||||||
|
DROP KEY `idx_user_id`,
|
||||||
|
MODIFY COLUMN `channel_type` ENUM('web_widget','api') NOT NULL DEFAULT 'web_widget' COMMENT '通道类型',
|
||||||
|
MODIFY COLUMN `status` ENUM('pending','active','suspended','disabled') NOT NULL DEFAULT 'pending' COMMENT '状态';
|
||||||
|
DROP TABLE IF EXISTS `fa_chathub_order`;
|
||||||
|
DROP TABLE IF EXISTS `fa_chathub_channel_account`;
|
||||||
|
DROP TABLE IF EXISTS `fa_chathub_gateway_log`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `provisioning` status was added because the previous 4-state machine (pending→active→suspended→disabled) had no slot for "paid but Chatwoot resources not yet provisioned". After payNotify, the tenant enters `provisioning` briefly while the chathub-provision service creates Inbox/Team/Agent, then transitions to `active`. If provisioning fails, status reverts to `pending` and the user can click "重新开通" to retry.
|
||||||
|
- The `channel_type` enum now has 7 values. The original 2 (web_widget/api) are still functional; the 5 new ones (amazon/jd/taobao/pdd/tiktok) are routed through `gateway/` Python library on the WS agent side.
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
1. **Single-file deploys** — the entire UI for `my()` and `register()` is in `controller/Index.php`, no template files to keep in sync.
|
||||||
|
2. **Dynamic flash messages** — the `FLASH_MESSAGE` placeholder is replaced via `str_replace` at the end of `my()` based on query string params (`?just_paid=1` / `?provisioning=1` / `?pending=1` / `?error=...`).
|
||||||
|
3. **No template-engine dependency** — works on any ThinkPHP 5 install without `think-template` package.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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: `_markOrderPaid` calls `_provisionAsync` on empty `embed_code`; new `reprovision` action; `payReturn` smart redirect (3 branches); `provisioning` state 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
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
/* ChatHub 租户管理样式 */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--chathub-primary: #6366f1;
|
||||||
|
--chathub-primary-dark: #4f46e5;
|
||||||
|
--chathub-success: #10b981;
|
||||||
|
--chathub-warning: #f59e0b;
|
||||||
|
--chathub-danger: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局过渡效果 */
|
||||||
|
.chathub-dashboard *,
|
||||||
|
.chathub-list *,
|
||||||
|
.chathub-form * {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-up {
|
||||||
|
animation: fadeInUp 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 脉冲动画 */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chathub-dashboard {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chathub-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chathub-header h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-options {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条美化 */
|
||||||
|
.chathub-dashboard::-webkit-scrollbar,
|
||||||
|
.chathub-list::-webkit-scrollbar,
|
||||||
|
.chathub-form::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chathub-dashboard::-webkit-scrollbar-track,
|
||||||
|
.chathub-list::-webkit-scrollbar-track,
|
||||||
|
.chathub-form::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chathub-dashboard::-webkit-scrollbar-thumb,
|
||||||
|
.chathub-list::-webkit-scrollbar-thumb,
|
||||||
|
.chathub-form::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chathub-dashboard::-webkit-scrollbar-thumb:hover,
|
||||||
|
.chathub-list::-webkit-scrollbar-thumb:hover,
|
||||||
|
.chathub-form::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具提示 */
|
||||||
|
[data-tooltip] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:hover::after {
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #1f2937;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格行悬停效果 */
|
||||||
|
.table tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态徽章动画 */
|
||||||
|
.status-badge {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮加载状态 */
|
||||||
|
.btn.loading {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.loading::after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid rgba(255,255,255,0.3);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.6s linear infinite;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 成功/失败动画 */
|
||||||
|
.success-animation {
|
||||||
|
animation: successPulse 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes successPulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态图标旋转 */
|
||||||
|
.empty-state i {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state:hover i {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
/**
|
||||||
|
* ChatHub 租户管理 JavaScript
|
||||||
|
*/
|
||||||
|
define(['jquery', 'bootstrap', 'template'], function ($, Bootstrap, Template) {
|
||||||
|
var Controller = {
|
||||||
|
index: function () {
|
||||||
|
// 初始化表格
|
||||||
|
Controller.api.initTable();
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
Controller.api.bindEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
add: function () {
|
||||||
|
// 初始化表单
|
||||||
|
Controller.api.initForm();
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function () {
|
||||||
|
// 初始化表单
|
||||||
|
Controller.api.initForm();
|
||||||
|
|
||||||
|
// 填充数据
|
||||||
|
Controller.api.fillFormData();
|
||||||
|
},
|
||||||
|
|
||||||
|
dashboard: function () {
|
||||||
|
// 初始化仪表盘
|
||||||
|
Controller.api.initDashboard();
|
||||||
|
},
|
||||||
|
|
||||||
|
api: {
|
||||||
|
// 初始化表格
|
||||||
|
initTable: function () {
|
||||||
|
// 加载租户列表
|
||||||
|
Controller.api.loadTenants();
|
||||||
|
|
||||||
|
// 绑定筛选事件
|
||||||
|
$('.filter-select, .filter-input').on('change', function () {
|
||||||
|
Controller.api.loadTenants(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载租户列表
|
||||||
|
loadTenants: function (page) {
|
||||||
|
page = page || 1;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: Fast.api.fixurl('chathub/index/index'),
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
page: page,
|
||||||
|
filter: JSON.stringify(Controller.api.getFilters())
|
||||||
|
},
|
||||||
|
success: function (res) {
|
||||||
|
if (res.rows && res.rows.length > 0) {
|
||||||
|
Controller.api.renderTenants(res.rows);
|
||||||
|
Controller.api.renderPagination(res.total, page);
|
||||||
|
$('#empty-state').hide();
|
||||||
|
$('.table-container table').show();
|
||||||
|
$('.pagination-wrapper').show();
|
||||||
|
} else {
|
||||||
|
$('#empty-state').show();
|
||||||
|
$('.table-container table').hide();
|
||||||
|
$('.pagination-wrapper').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取筛选条件
|
||||||
|
getFilters: function () {
|
||||||
|
return {
|
||||||
|
status: $('.filter-select').eq(0).val(),
|
||||||
|
channel_type: $('.filter-select').eq(1).val(),
|
||||||
|
search: $('.filter-input').val()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// 渲染租户列表
|
||||||
|
renderTenants: function (tenants) {
|
||||||
|
var html = '';
|
||||||
|
tenants.forEach(function (tenant) {
|
||||||
|
var statusClass = 'status-' + tenant.status;
|
||||||
|
var channelIcon = tenant.channel_type === 'web_widget' ? 'fa-globe' : 'fa-code';
|
||||||
|
var channelText = tenant.channel_type_text || (tenant.channel_type === 'web_widget' ? '网页组件' : 'API接口');
|
||||||
|
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td>';
|
||||||
|
html += ' <div class="tenant-info">';
|
||||||
|
html += ' <div class="tenant-avatar">' + tenant.tenant_name.charAt(0).toUpperCase() + '</div>';
|
||||||
|
html += ' <div class="tenant-details">';
|
||||||
|
html += ' <h4>' + tenant.tenant_name + '</h4>';
|
||||||
|
html += ' <p>' + (tenant.agent_name || '未配置') + '</p>';
|
||||||
|
html += ' </div>';
|
||||||
|
html += ' </div>';
|
||||||
|
html += '</td>';
|
||||||
|
html += '<td>' + tenant.domain + '</td>';
|
||||||
|
html += '<td><span class="channel-tag"><i class="fa ' + channelIcon + '"></i> ' + channelText + '</span></td>';
|
||||||
|
html += '<td><span class="status-badge ' + statusClass + '">' + tenant.status_text + '</span></td>';
|
||||||
|
html += '<td>' + (tenant.provisioned_at || '-') + '</td>';
|
||||||
|
html += '<td>';
|
||||||
|
html += ' <div class="action-buttons">';
|
||||||
|
html += ' <a href="' + Fast.api.fixurl('chathub/index/edit/ids/' + tenant.id) + '" class="action-btn action-btn-edit" title="编辑"><i class="fa fa-pencil"></i></a>';
|
||||||
|
if (tenant.status !== 'active') {
|
||||||
|
html += ' <button class="action-btn action-btn-provision" title="开通" onclick="Controller.api.provisionTenant(' + tenant.id + ')"><i class="fa fa-rocket"></i></button>';
|
||||||
|
}
|
||||||
|
html += ' <button class="action-btn action-btn-delete" title="删除" onclick="Controller.api.deleteTenant(' + tenant.id + ')"><i class="fa fa-trash"></i></button>';
|
||||||
|
html += ' </div>';
|
||||||
|
html += '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
$('#tenant-list').html(html);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 渲染分页
|
||||||
|
renderPagination: function (total, currentPage) {
|
||||||
|
var pageSize = 10;
|
||||||
|
var totalPages = Math.ceil(total / pageSize);
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
if (currentPage > 1) {
|
||||||
|
html += '<span class="page-link" onclick="Controller.api.loadTenants(' + (currentPage - 1) + ')">上一页</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 1; i <= totalPages; i++) {
|
||||||
|
if (i === currentPage) {
|
||||||
|
html += '<span class="page-link active">' + i + '</span>';
|
||||||
|
} else {
|
||||||
|
html += '<span class="page-link" onclick="Controller.api.loadTenants(' + i + ')">' + i + '</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
html += '<span class="page-link" onclick="Controller.api.loadTenants(' + (currentPage + 1) + ')">下一页</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#pagination').html(html);
|
||||||
|
$('#pagination-info').text('显示 ' + ((currentPage-1)*pageSize+1) + '-' + Math.min(currentPage*pageSize, total) + ' 条,共 ' + total + ' 条');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 开通租户
|
||||||
|
provisionTenant: function (id) {
|
||||||
|
if (confirm('确定要开通这个租户吗?')) {
|
||||||
|
Fast.api.ajax({
|
||||||
|
url: Fast.api.fixurl('chathub/index/provision/ids/' + id),
|
||||||
|
type: 'POST'
|
||||||
|
}, function (res) {
|
||||||
|
Controller.api.loadTenants();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除租户
|
||||||
|
deleteTenant: function (id) {
|
||||||
|
if (confirm('确定要删除这个租户吗?此操作不可恢复。')) {
|
||||||
|
Fast.api.ajax({
|
||||||
|
url: Fast.api.fixurl('chathub/index/del/ids/' + id),
|
||||||
|
type: 'POST'
|
||||||
|
}, function (res) {
|
||||||
|
Controller.api.loadTenants();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化表单
|
||||||
|
initForm: function () {
|
||||||
|
// 通道类型选择
|
||||||
|
$('.channel-option').click(function () {
|
||||||
|
$('.channel-option').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
$('input[name="row[channel_type]"]').val($(this).data('value'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单提交
|
||||||
|
$('#tenant-form').on('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var formData = $(this).serialize();
|
||||||
|
var url = $(this).attr('action');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'POST',
|
||||||
|
data: formData,
|
||||||
|
success: function (res) {
|
||||||
|
if (res.code === 1) {
|
||||||
|
Fast.api.msg('操作成功!');
|
||||||
|
setTimeout(function () {
|
||||||
|
location.href = Fast.api.fixurl('chathub/index/index');
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
Fast.api.msg(res.msg || '操作失败', 'danger');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
Fast.api.msg('网络错误,请重试', 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 填充表单数据
|
||||||
|
fillFormData: function () {
|
||||||
|
// 从页面获取数据并填充
|
||||||
|
var row = window.rowData || {};
|
||||||
|
|
||||||
|
if (row.tenant_name) {
|
||||||
|
$('input[name="row[tenant_name]"]').val(row.tenant_name);
|
||||||
|
}
|
||||||
|
if (row.domain) {
|
||||||
|
$('input[name="row[domain]"]').val(row.domain);
|
||||||
|
}
|
||||||
|
if (row.agent_name) {
|
||||||
|
$('input[name="row[agent_name]"]').val(row.agent_name);
|
||||||
|
}
|
||||||
|
if (row.status) {
|
||||||
|
$('select[name="row[status]"]').val(row.status);
|
||||||
|
}
|
||||||
|
if (row.channel_type) {
|
||||||
|
$('.channel-option').removeClass('active');
|
||||||
|
$('.channel-option[data-value="' + row.channel_type + '"]').addClass('active');
|
||||||
|
$('input[name="row[channel_type]"]').val(row.channel_type);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化仪表盘
|
||||||
|
initDashboard: function () {
|
||||||
|
// 添加动画效果
|
||||||
|
$('.stat-card').each(function (index) {
|
||||||
|
$(this).css({
|
||||||
|
'opacity': '0',
|
||||||
|
'transform': 'translateY(20px)',
|
||||||
|
'animation': 'fadeInUp 0.5s ease forwards',
|
||||||
|
'animation-delay': (index * 0.1) + 's'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
bindEvents: function () {
|
||||||
|
// 刷新按钮
|
||||||
|
$(document).on('click', '.btn-refresh', function () {
|
||||||
|
Controller.api.loadTenants();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Controller;
|
||||||
|
});
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'name' => 'provision_server_url',
|
||||||
|
'title' => 'Provision 服务地址',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => '系统',
|
||||||
|
'content' => [],
|
||||||
|
'value' => 'http://CoPaw:5566',
|
||||||
|
'rule' => 'required',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => 'QwenPaw 环境内的 Provision HTTP 服务地址',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'site_name',
|
||||||
|
'title' => '站点名称',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => '系统',
|
||||||
|
'content' => [],
|
||||||
|
'value' => 'ChatHub',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '产品名称,显示在页面标题和导航',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'support_email',
|
||||||
|
'title' => '客服邮箱',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => '系统',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => 'email',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '用户联系邮箱,注册成功通知会发到这里',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'chatwoot_url',
|
||||||
|
'title' => 'Chatwoot 地址',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => 'Chatwoot',
|
||||||
|
'content' => [],
|
||||||
|
'value' => 'https://chatwoot.275763.xyz',
|
||||||
|
'rule' => 'required',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => 'Chatwoot 自托管地址',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'chatwoot_api_token',
|
||||||
|
'title' => 'Chatwoot API Token',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => 'Chatwoot',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => 'required',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => 'Chatwoot 个人 Access Token(设置 → Profile → Access Token)',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'alipay_enabled',
|
||||||
|
'title' => '启用支付宝',
|
||||||
|
'type' => 'switch',
|
||||||
|
'group' => '支付宝',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '0',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '开启后注册/续费页显示支付宝支付选项',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'alipay_app_id',
|
||||||
|
'title' => 'APPID',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => '支付宝',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '支付宝开放平台应用 APPID(沙箱/生产不同)',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'alipay_merchant_private_key',
|
||||||
|
'title' => '应用私钥',
|
||||||
|
'type' => 'textarea',
|
||||||
|
'group' => '支付宝',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => 'RSA2 私钥(应用公钥对应的私钥),-----BEGIN PRIVATE KEY----- 开头',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'alipay_public_key',
|
||||||
|
'title' => '支付宝公钥',
|
||||||
|
'type' => 'textarea',
|
||||||
|
'group' => '支付宝',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '支付宝公钥(不是应用公钥),用于验证回调签名',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'alipay_sandbox',
|
||||||
|
'title' => '沙箱模式',
|
||||||
|
'type' => 'switch',
|
||||||
|
'group' => '支付宝',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '1',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '开启=沙箱环境(测试用),关闭=正式环境',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'wechat_enabled',
|
||||||
|
'title' => '启用微信支付',
|
||||||
|
'type' => 'switch',
|
||||||
|
'group' => '微信支付',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '0',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '开启后注册/续费页显示微信支付选项',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'wechat_app_id',
|
||||||
|
'title' => 'APPID',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => '微信支付',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '微信开放平台/公众平台 APPID',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'wechat_mch_id',
|
||||||
|
'title' => '商户号',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => '微信支付',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '微信支付商户号',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'wechat_api_v3_key',
|
||||||
|
'title' => 'APIv3 密钥',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => '微信支付',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '微信支付 APIv3 密钥(商户平台 → API安全 → APIv3密钥)',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'wechat_cert_path',
|
||||||
|
'title' => '商户证书路径',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => '微信支付',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => 'apiclient_cert.pem 绝对路径,例如 /www/sites/hub/certs/apiclient_cert.pem',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'wechat_key_path',
|
||||||
|
'title' => '商户私钥路径',
|
||||||
|
'type' => 'string',
|
||||||
|
'group' => '微信支付',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => 'apiclient_key.pem 绝对路径',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'wechat_sandbox',
|
||||||
|
'title' => '测试模式',
|
||||||
|
'type' => 'switch',
|
||||||
|
'group' => '微信支付',
|
||||||
|
'content' => [],
|
||||||
|
'value' => '1',
|
||||||
|
'rule' => '',
|
||||||
|
'msg' => '',
|
||||||
|
'tip' => '开启=测试号环境,关闭=正式环境',
|
||||||
|
'ok' => '',
|
||||||
|
'extend' => '',
|
||||||
|
],
|
||||||
|
];
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
name = chathub
|
||||||
|
title = ChatHub 租户管理
|
||||||
|
intro = Chatwoot AI客服系统租户管理平台,支持多租户开通、配置管理、状态监控
|
||||||
|
author = GreatQiu
|
||||||
|
website = https://github.com/hanmolabiqiu/chatwoot-ai-agent
|
||||||
|
version = 1.0.0
|
||||||
|
state = 1
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `__PREFIX__chathub_tenant` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`user_id` int(11) unsigned DEFAULT NULL COMMENT 'fa_user.id (创建者)',
|
||||||
|
`tenant_name` varchar(100) NOT NULL DEFAULT '' COMMENT '租户名称',
|
||||||
|
`domain` varchar(255) NOT NULL DEFAULT '' COMMENT '域名',
|
||||||
|
`email` varchar(255) NOT NULL DEFAULT '' COMMENT 'Chatwoot登录邮箱',
|
||||||
|
`inbox_id` int(11) DEFAULT NULL COMMENT 'Chatwoot Inbox ID',
|
||||||
|
`inbox_token` varchar(100) DEFAULT NULL COMMENT 'Chatwoot Inbox Token',
|
||||||
|
`team_id` int(11) DEFAULT NULL COMMENT 'Chatwoot Team ID',
|
||||||
|
`agent_id` varchar(100) DEFAULT NULL COMMENT 'QwenPaw Agent ID',
|
||||||
|
`agent_cw_id` int(11) DEFAULT NULL COMMENT 'Chatwoot Agent用户ID',
|
||||||
|
`agent_cw_password` varchar(100) DEFAULT NULL COMMENT 'Chatwoot初始密码',
|
||||||
|
`agent_name` varchar(100) DEFAULT NULL COMMENT 'QwenPaw Agent 名称',
|
||||||
|
`max_agents` int(11) NOT NULL DEFAULT 3 COMMENT '最大坐席数',
|
||||||
|
`channel_type` enum('web_widget','api','amazon','jd','taobao','pdd','tiktok') NOT NULL DEFAULT 'web_widget' COMMENT '通道类型',
|
||||||
|
`status` enum('pending','provisioning','active','suspended','disabled') NOT NULL DEFAULT 'pending' COMMENT '状态',
|
||||||
|
`config` text COMMENT '配置JSON',
|
||||||
|
`embed_code` text COMMENT '嵌入代码',
|
||||||
|
`api_credentials` text COMMENT 'API凭据JSON',
|
||||||
|
`provisioned_at` datetime DEFAULT NULL COMMENT '开通时间',
|
||||||
|
`expire_at` datetime DEFAULT NULL COMMENT '到期时间',
|
||||||
|
`last_active_at` datetime DEFAULT NULL COMMENT '最后活跃时间',
|
||||||
|
`createtime` int(10) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_domain` (`domain`),
|
||||||
|
KEY `idx_status` (`status`),
|
||||||
|
KEY `idx_inbox_id` (`inbox_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ChatHub租户表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `__PREFIX__chathub_log` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`tenant_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '租户ID (0=系统级)',
|
||||||
|
`action` varchar(50) NOT NULL DEFAULT '' COMMENT '操作类型 (register/renew/pay_notify_*/reprovision/...)',
|
||||||
|
`detail` text COMMENT '操作详情',
|
||||||
|
`status` enum('success','failed') NOT NULL DEFAULT 'success' COMMENT '状态',
|
||||||
|
`operator` varchar(100) DEFAULT NULL COMMENT '操作人 (用户邮箱或system)',
|
||||||
|
`createtime` int(10) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_tenant_id` (`tenant_id`),
|
||||||
|
KEY `idx_action` (`action`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ChatHub操作日志表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `__PREFIX__chathub_order` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`order_no` varchar(50) NOT NULL COMMENT '订单号',
|
||||||
|
`tenant_id` int(11) unsigned NOT NULL COMMENT '租户ID',
|
||||||
|
`user_id` int(11) unsigned NOT NULL COMMENT '下单用户ID',
|
||||||
|
`plan` varchar(20) NOT NULL COMMENT '方案 (basic/pro/enterprise)',
|
||||||
|
`amount` decimal(10,2) NOT NULL COMMENT '金额',
|
||||||
|
`pay_method` varchar(20) DEFAULT NULL COMMENT '支付方式 (alipay/wechat)',
|
||||||
|
`pay_trade_no` varchar(100) DEFAULT NULL COMMENT '支付平台交易号',
|
||||||
|
`status` enum('pending','paid','failed','refunded') NOT NULL DEFAULT 'pending' COMMENT '状态',
|
||||||
|
`paid_at` datetime DEFAULT NULL COMMENT '支付完成时间',
|
||||||
|
`expire_at` datetime DEFAULT NULL COMMENT '续期到期时间',
|
||||||
|
`createtime` int(10) unsigned NOT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` int(10) unsigned DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_order_no` (`order_no`),
|
||||||
|
KEY `idx_tenant_id` (`tenant_id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ChatHub订单表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `__PREFIX__chathub_channel_account` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`tenant_id` int(11) unsigned NOT NULL COMMENT '租户ID',
|
||||||
|
`channel` enum('amazon','jd','taobao','pdd','tiktok') NOT NULL COMMENT '平台',
|
||||||
|
`shop_id` varchar(100) DEFAULT NULL COMMENT '店铺ID',
|
||||||
|
`shop_name` varchar(200) DEFAULT NULL COMMENT '店铺名称',
|
||||||
|
`credentials_encrypted` varbinary(2048) NOT NULL COMMENT 'AES-256-GCM 加密凭据',
|
||||||
|
`expires_at` datetime DEFAULT NULL COMMENT 'Access Token 过期时间',
|
||||||
|
`status` enum('active','expired','error','paused') DEFAULT 'active' COMMENT '状态',
|
||||||
|
`last_error` text COMMENT '最后一次错误',
|
||||||
|
`last_refresh_at` datetime DEFAULT NULL COMMENT '上次刷新时间',
|
||||||
|
`rate_limit_per_sec` int(10) unsigned DEFAULT 5 COMMENT '每秒请求上限',
|
||||||
|
`createtime` int(10) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_tenant_id` (`tenant_id`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ChatHub渠道账号表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `__PREFIX__chathub_gateway_log` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`tenant_id` int(11) unsigned NOT NULL COMMENT '租户ID',
|
||||||
|
`channel` enum('amazon','jd','taobao','pdd','tiktok') NOT NULL COMMENT '平台',
|
||||||
|
`query_hash` char(64) NOT NULL COMMENT '查询内容 SHA256',
|
||||||
|
`status` enum('success','cache_hit','rate_limited','breaker_open','error','timeout','no_creds') NOT NULL COMMENT '调用结果',
|
||||||
|
`latency_ms` int(10) unsigned DEFAULT NULL COMMENT '延迟(毫秒)',
|
||||||
|
`error_msg` varchar(500) DEFAULT NULL COMMENT '错误信息',
|
||||||
|
`createtime` int(10) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_tenant_id` (`tenant_id`),
|
||||||
|
KEY `idx_channel_status` (`channel`,`status`),
|
||||||
|
KEY `idx_createtime` (`createtime`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ChatHub Gateway调用日志';
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
namespace addons\chathub\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class ChathubTenant extends Model
|
||||||
|
{
|
||||||
|
// 表名
|
||||||
|
protected $name = 'chathub_tenant';
|
||||||
|
|
||||||
|
// 自动写入时间戳
|
||||||
|
protected $autoWriteTimestamp = 'int';
|
||||||
|
|
||||||
|
// 定义时间戳字段名
|
||||||
|
protected $createTime = 'createtime';
|
||||||
|
protected $updateTime = 'updatetime';
|
||||||
|
|
||||||
|
// 追加属性
|
||||||
|
protected $append = [
|
||||||
|
'status_text',
|
||||||
|
'channel_type_text',
|
||||||
|
'team_id_text',
|
||||||
|
'email_text'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态文本
|
||||||
|
*/
|
||||||
|
public function getStatusTextAttr($value, $data)
|
||||||
|
{
|
||||||
|
$status = ['pending' => '待开通', 'active' => '正常', 'suspended' => '已暂停', 'disabled' => '已禁用'];
|
||||||
|
return $status[$data['status']] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通道类型文本
|
||||||
|
*/
|
||||||
|
public function getChannelTypeTextAttr($value, $data)
|
||||||
|
{
|
||||||
|
$types = ['web_widget' => '网页组件', 'api' => 'API接口'];
|
||||||
|
return $types[$data['channel_type']] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置JSON解析
|
||||||
|
*/
|
||||||
|
public function getConfigAttr($value)
|
||||||
|
{
|
||||||
|
return $value ? json_decode($value, true) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConfigAttr($value)
|
||||||
|
{
|
||||||
|
return json_encode($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API凭据JSON解析
|
||||||
|
*/
|
||||||
|
public function getApiCredentialsAttr($value)
|
||||||
|
{
|
||||||
|
return $value ? json_decode($value, true) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setApiCredentialsAttr($value)
|
||||||
|
{
|
||||||
|
return json_encode($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团队ID文本
|
||||||
|
*/
|
||||||
|
public function getTeamIdTextAttr($value, $data)
|
||||||
|
{
|
||||||
|
if (empty($data['team_id'])) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return '#' . $data['team_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱文本(脱敏显示)
|
||||||
|
*/
|
||||||
|
public function getEmailTextAttr($value, $data)
|
||||||
|
{
|
||||||
|
if (empty($data['email'])) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
$parts = explode('@', $data['email']);
|
||||||
|
if (count($parts) === 2) {
|
||||||
|
$name = $parts[0];
|
||||||
|
$len = strlen($name);
|
||||||
|
$masked = substr($name, 0, min(2, $len)) . str_repeat('*', max(0, $len - 2));
|
||||||
|
return $masked . '@' . $parts[1];
|
||||||
|
}
|
||||||
|
return $data['email'];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user