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:
@@ -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