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:
GreatQiu
2026-06-05 14:20:00 +08:00
parent 1d620ede9b
commit 91104e58cf
13 changed files with 3385 additions and 0 deletions
+68
View File
@@ -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
}
}
+21
View File
@@ -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.
+69
View File
@@ -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.
+146
View File
@@ -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.0v1.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
+203
View File
@@ -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);
}
+251
View File
@@ -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;
});
+225
View File
@@ -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
+7
View File
@@ -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
+98
View File
@@ -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调用日志';
+98
View File
@@ -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'];
}
}