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
+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;
});