1.新增菜单后台操作指南,方便管理员查看使用
26
saiadmin-artd/src/api/system/admin_guide.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import request from '@/utils/http'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台操作指南 API
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* 读取 Markdown 内容
|
||||||
|
*/
|
||||||
|
read() {
|
||||||
|
return request.get<Api.Common.ApiData>({
|
||||||
|
url: '/core/adminGuide/read'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 Markdown 内容
|
||||||
|
*/
|
||||||
|
save(params: { content: string }) {
|
||||||
|
return request.post<Api.Common.ApiData>({
|
||||||
|
url: '/core/adminGuide/save',
|
||||||
|
data: params,
|
||||||
|
showSuccessMessage: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
20
saiadmin-artd/src/locales/langs/en/system/admin_guide.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"title": "Admin Guide",
|
||||||
|
"toolbar": {
|
||||||
|
"edit": "Edit",
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"refresh": "Refresh"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"filePath": "File Path",
|
||||||
|
"updateTime": "Updated At"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"loadFailed": "Failed to load admin guide",
|
||||||
|
"saveSuccess": "Saved successfully",
|
||||||
|
"saveFailed": "Failed to save",
|
||||||
|
"cancelConfirm": "You have unsaved changes. Cancel editing?",
|
||||||
|
"editRequired": "Please click Edit before saving"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
saiadmin-artd/src/locales/langs/zh/system/admin_guide.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"title": "后台操作指南",
|
||||||
|
"toolbar": {
|
||||||
|
"edit": "编辑",
|
||||||
|
"save": "保存",
|
||||||
|
"cancel": "取消",
|
||||||
|
"refresh": "刷新"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"filePath": "文档路径",
|
||||||
|
"updateTime": "更新时间"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"loadFailed": "加载操作指南失败",
|
||||||
|
"saveSuccess": "保存成功",
|
||||||
|
"saveFailed": "保存失败",
|
||||||
|
"cancelConfirm": "当前有未保存的修改,确定取消编辑吗?",
|
||||||
|
"editRequired": "请先点击编辑后再保存"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ const NO_CHANNEL_LAYOUT_PATHS = [
|
|||||||
'/safeguard/database',
|
'/safeguard/database',
|
||||||
'/safeguard/server',
|
'/safeguard/server',
|
||||||
'/safeguard/cache',
|
'/safeguard/cache',
|
||||||
'/safeguard/email-log'
|
'/safeguard/email-log',
|
||||||
|
'/admin_guide'
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 日志页:左侧首项为「全部」,dept_id=0 表示不按渠道过滤 */
|
/** 日志页:左侧首项为「全部」,dept_id=0 表示不按渠道过滤 */
|
||||||
|
|||||||
211
saiadmin-artd/src/views/system/admin_guide/index.vue
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div class="art-full-height admin-guide-page">
|
||||||
|
<ElCard class="art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<b>{{ $t('page.title') }}</b>
|
||||||
|
<div v-if="meta.filePath" class="mt-1 text-xs text-g-500">
|
||||||
|
{{ $t('page.meta.filePath') }}:{{ meta.filePath }}
|
||||||
|
<span v-if="meta.updateTime" class="ml-3">
|
||||||
|
{{ $t('page.meta.updateTime') }}:{{ meta.updateTime }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElSpace wrap>
|
||||||
|
<ElButton
|
||||||
|
v-permission="'system:admin_guide:index:read'"
|
||||||
|
:loading="loading"
|
||||||
|
@click="loadContent"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<ArtSvgIcon icon="ri:refresh-line" />
|
||||||
|
</template>
|
||||||
|
{{ $t('page.toolbar.refresh') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="!isEditing"
|
||||||
|
v-permission="'system:admin_guide:index:edit'"
|
||||||
|
type="primary"
|
||||||
|
@click="startEdit"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<ArtSvgIcon icon="ri:pencil-line" />
|
||||||
|
</template>
|
||||||
|
{{ $t('page.toolbar.edit') }}
|
||||||
|
</ElButton>
|
||||||
|
<template v-if="isEditing">
|
||||||
|
<ElButton @click="handleCancel">
|
||||||
|
{{ $t('page.toolbar.cancel') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-permission="'system:admin_guide:index:save'"
|
||||||
|
type="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<ArtSvgIcon icon="ri:save-line" />
|
||||||
|
</template>
|
||||||
|
{{ $t('page.toolbar.save') }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElSpace>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="admin-guide-body flex-1 min-h-0 overflow-auto">
|
||||||
|
<SaMdEditor
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="editContent"
|
||||||
|
height="calc(100vh - 220px)"
|
||||||
|
min-height="480px"
|
||||||
|
/>
|
||||||
|
<MdPreview
|
||||||
|
v-else
|
||||||
|
:model-value="previewContent"
|
||||||
|
:theme="previewTheme"
|
||||||
|
preview-theme="github"
|
||||||
|
class="admin-guide-preview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { MdPreview } from 'md-editor-v3'
|
||||||
|
import 'md-editor-v3/lib/preview.css'
|
||||||
|
import SaMdEditor from '@/components/sai/sa-md-editor/index.vue'
|
||||||
|
import { useSettingStore } from '@/store/modules/setting'
|
||||||
|
import api from '@/api/system/admin_guide'
|
||||||
|
|
||||||
|
defineOptions({ name: 'SystemAdminGuide' })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const previewContent = ref('')
|
||||||
|
const editContent = ref('')
|
||||||
|
const originalContent = ref('')
|
||||||
|
const meta = ref<{ filePath?: string; updateTime?: string }>({})
|
||||||
|
|
||||||
|
const previewTheme = computed(() => (settingStore.isDark ? 'dark' : 'light'))
|
||||||
|
|
||||||
|
/** 静态资源根地址(public 目录,开发环境走代理地址) */
|
||||||
|
const getStaticFileBase = (): string => {
|
||||||
|
const apiUrl = import.meta.env.VITE_API_URL || ''
|
||||||
|
if (apiUrl.startsWith('http')) {
|
||||||
|
return apiUrl.replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
const proxyUrl = import.meta.env.VITE_API_PROXY_URL || ''
|
||||||
|
if (proxyUrl) {
|
||||||
|
return String(proxyUrl).replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveGuideImages = (content: string): string => {
|
||||||
|
const staticBase = getStaticFileBase()
|
||||||
|
if (!staticBase) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
return content.replace(/!\[([^\]]*)\]\((\/docs\/picture\/[^)]+)\)/g, (_match, alt, path) => {
|
||||||
|
return ``
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadContent = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.read()
|
||||||
|
const data = res as { content?: string; file_path?: string; update_time?: string }
|
||||||
|
const rawContent = data.content ?? ''
|
||||||
|
previewContent.value = resolveGuideImages(rawContent)
|
||||||
|
editContent.value = rawContent
|
||||||
|
originalContent.value = rawContent
|
||||||
|
meta.value = {
|
||||||
|
filePath: data.file_path,
|
||||||
|
updateTime: data.update_time
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 错误由 http 工具统一处理
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEdit = () => {
|
||||||
|
editContent.value = previewContent.value
|
||||||
|
originalContent.value = previewContent.value
|
||||||
|
isEditing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = async () => {
|
||||||
|
if (editContent.value !== originalContent.value) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(t('page.message.cancelConfirm'), {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editContent.value = originalContent.value
|
||||||
|
isEditing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!isEditing.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.save({ content: editContent.value })
|
||||||
|
const data = res as { content?: string; file_path?: string; update_time?: string }
|
||||||
|
const savedContent = data.content ?? editContent.value
|
||||||
|
editContent.value = savedContent
|
||||||
|
originalContent.value = savedContent
|
||||||
|
previewContent.value = resolveGuideImages(savedContent)
|
||||||
|
meta.value = {
|
||||||
|
filePath: data.file_path ?? meta.value.filePath,
|
||||||
|
updateTime: data.update_time ?? meta.value.updateTime
|
||||||
|
}
|
||||||
|
isEditing.value = false
|
||||||
|
} catch {
|
||||||
|
// 错误由 http 工具统一处理
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadContent()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.admin-guide-page {
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-guide-body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-guide-preview {
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
107
server/db/run_system_admin_guide_menu.php
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 安装「后台操作指南」菜单与权限,并授权给超级管理员角色
|
||||||
|
* 用法(在 server 目录): php db/run_system_admin_guide_menu.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../support/bootstrap.php';
|
||||||
|
|
||||||
|
use plugin\saiadmin\app\cache\UserMenuCache;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
function runSqlFile(PDO $pdo, string $path, string $label): void
|
||||||
|
{
|
||||||
|
echo "\n=== {$label} ===\n";
|
||||||
|
if (! is_file($path)) {
|
||||||
|
echo "跳过:文件不存在 {$path}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$sql = file_get_contents($path);
|
||||||
|
$sql = preg_replace('/--.*$/m', '', $sql);
|
||||||
|
$parts = array_filter(array_map('trim', explode(';', $sql)));
|
||||||
|
$ok = 0;
|
||||||
|
foreach ($parts as $statement) {
|
||||||
|
if ($statement === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$pdo->exec($statement);
|
||||||
|
$ok++;
|
||||||
|
}
|
||||||
|
echo "完成:执行 {$ok} 条语句\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function cliPdo(): PDO
|
||||||
|
{
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$port = getenv('DB_PORT') ?: '3306';
|
||||||
|
$db = getenv('DB_NAME') ?: '';
|
||||||
|
$user = getenv('DB_USER') ?: '';
|
||||||
|
$pass = getenv('DB_PASSWORD') ?: '';
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$db};charset=utf8mb4";
|
||||||
|
return new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "========== 后台操作指南菜单安装 ==========\n";
|
||||||
|
echo '数据库: ' . (getenv('DB_NAME') ?: '') . '@' . (getenv('DB_HOST') ?: '') . "\n";
|
||||||
|
|
||||||
|
$pdo = cliPdo();
|
||||||
|
runSqlFile($pdo, __DIR__ . '/system_admin_guide_menu.sql', '1. 菜单与按钮权限');
|
||||||
|
|
||||||
|
$menuId = (int) Db::name('sa_system_menu')
|
||||||
|
->where('path', 'admin_guide')
|
||||||
|
->where('component', '/system/admin_guide/index')
|
||||||
|
->where('type', 2)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($menuId <= 0) {
|
||||||
|
echo "错误:未找到后台操作指南菜单\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$buttonIds = Db::name('sa_system_menu')
|
||||||
|
->where('parent_id', $menuId)
|
||||||
|
->where('type', 3)
|
||||||
|
->column('id');
|
||||||
|
|
||||||
|
$allMenuIds = array_values(array_unique(array_merge([$menuId], array_map('intval', $buttonIds ?: []))));
|
||||||
|
|
||||||
|
echo "\n=== 2. 授权超级管理员角色 ===\n";
|
||||||
|
$adminRoleIds = Db::name('sa_system_role')
|
||||||
|
->where('code', 'super_admin')
|
||||||
|
->column('id');
|
||||||
|
|
||||||
|
if ($adminRoleIds === [] || $adminRoleIds === null) {
|
||||||
|
$adminRoleIds = Db::name('sa_system_role')->where('id', 1)->column('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
$inserted = 0;
|
||||||
|
foreach ($adminRoleIds as $roleId) {
|
||||||
|
$roleId = (int) $roleId;
|
||||||
|
foreach ($allMenuIds as $mid) {
|
||||||
|
$exists = Db::name('sa_system_role_menu')
|
||||||
|
->where('role_id', $roleId)
|
||||||
|
->where('menu_id', $mid)
|
||||||
|
->count();
|
||||||
|
if ($exists > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Db::name('sa_system_role_menu')->insert([
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'menu_id' => $mid,
|
||||||
|
]);
|
||||||
|
$inserted++;
|
||||||
|
}
|
||||||
|
echo " 角色 {$roleId}:新增授权 {$inserted} 条\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
UserMenuCache::clearMenuCache();
|
||||||
|
\plugin\saiadmin\app\cache\UserAuthCache::clear();
|
||||||
|
|
||||||
|
echo "\n菜单 ID: {$menuId}\n";
|
||||||
|
echo "按钮权限: " . implode(',', $allMenuIds) . "\n";
|
||||||
|
echo "已清除菜单缓存,请重新登录后台查看。\n";
|
||||||
|
echo "========== 安装完成 ==========\n";
|
||||||
39
server/db/system_admin_guide_menu.sql
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
-- 后台操作指南顶级菜单与权限
|
||||||
|
-- 说明:挂载到顶级菜单(parent_id=0),内容来源 server/docs/ADMIN_GUIDE.md
|
||||||
|
|
||||||
|
SET @now = NOW();
|
||||||
|
|
||||||
|
-- 1) 创建后台操作指南顶级菜单(type=2,parent_id=0)
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`icon`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT 0, '后台操作指南', 'AdminGuide', NULL, 2, 'admin_guide', '/system/admin_guide/index', NULL, 'ri:book-read-line', 5, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sa_system_menu` WHERE `path` = 'admin_guide' AND `component` = '/system/admin_guide/index' AND `type` = 2
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @admin_guide_menu_id = (
|
||||||
|
SELECT `id` FROM `sa_system_menu`
|
||||||
|
WHERE `path` = 'admin_guide' AND `component` = '/system/admin_guide/index' AND `type` = 2
|
||||||
|
ORDER BY `id` ASC LIMIT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2) 创建按钮权限
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT @admin_guide_menu_id, '数据列表', '', 'system:admin_guide:index:index', 3, '', '', '', 100, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `sa_system_menu` WHERE `slug` = 'system:admin_guide:index:index' AND `type` = 3);
|
||||||
|
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT @admin_guide_menu_id, '读取', '', 'system:admin_guide:index:read', 3, '', '', '', 100, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `sa_system_menu` WHERE `slug` = 'system:admin_guide:index:read' AND `type` = 3);
|
||||||
|
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT @admin_guide_menu_id, '编辑', '', 'system:admin_guide:index:edit', 3, '', '', '', 100, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `sa_system_menu` WHERE `slug` = 'system:admin_guide:index:edit' AND `type` = 3);
|
||||||
|
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT @admin_guide_menu_id, '保存', '', 'system:admin_guide:index:save', 3, '', '', '', 100, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `sa_system_menu` WHERE `slug` = 'system:admin_guide:index:save' AND `type` = 3);
|
||||||
128
server/docs/ADMIN_GUIDE.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# 大富翁-使用说明指南
|
||||||
|
|
||||||
|
## 菜单简单介绍
|
||||||
|
|
||||||
|
## 工作台/统计页面:统计数据
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 角色管理:对角色的菜单权限设置
|
||||||
|
|
||||||
|
按等级设定,等级越低权限越少(不要出现上级角色没有的权限,子角色有)
|
||||||
|
避免方式:使用子角色创建下级角色,可以避免下级角色比上级角色操作权限更多的问题
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这里设置角色的菜单以及按钮权限
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 彩金池配置:监听彩金池实时变化
|
||||||
|
|
||||||
|
可以实时监听彩金池累积金额的变化
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 游戏配置:游戏规则和平台币转化比
|
||||||
|
|
||||||
|
游戏配置
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
其中游戏玩法为进入游戏的弹窗,和规则介绍(无特殊需求不需要大改)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
游戏平台币兑换币:为进入平台时平台比转化比,比如,当前设置的为1:1,如果从jk8平台转入100,那么获取的游戏币为100,如果设置1:2则获取的平台币为200
|
||||||
|
|
||||||
|
## 底注配置:方便玩家快速调整压注倍率
|
||||||
|
|
||||||
|
底注配置
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
对应游戏中的,其中每次游玩对局基础消耗为1游戏币(无法修改),底注的设置只是方便玩家快速修改压注金额
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 抽奖逻辑
|
||||||
|
|
||||||
|
## 判断抽奖档位
|
||||||
|
|
||||||
|
当前的抽奖逻辑时,按照抽奖档位(T1-T5)进行抽奖,在【玩家管理】菜单中的设置玩家具体的档位权重
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
也可以在【彩金池配置】菜单中设置玩家正常的抽奖档位权重
|
||||||
|
|
||||||
|
- 其中正常的档位权重为注册玩家默认绑定的档位权重,并且只有在修改完后,创建的玩家才能绑定最新的正常档位权重
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 其中free为杀分权重为如果当前彩金池(平台)盈利超过设置的安全线则制动走杀分的权重
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 剩余的两个权重可以方便快速切换用户的档位抽奖权重
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 根据档位抽取中奖号码
|
||||||
|
|
||||||
|
### 设置中奖号码地图
|
||||||
|
|
||||||
|
在后台设置地图缩影
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
地图的索引参看如下
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
其中地图的索引可以按照需求点击图中的按规则生成
|
||||||
|
|
||||||
|
并且规则尽可能符合:结算金额 < 0 → T4;0 < 结算金额 < 100 → T3;100 < 结算金额 < 200 → T2;200 < 结算金额 → T1;T5「再来一次」结算金额=0
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 创建完地图索引后创建相应的奖励对照表
|
||||||
|
|
||||||
|
创建奖励对照表的原因是由于有每个号码的权重不一样,豹子号10,15,20,25有多重组合方式,所以需要设置奖励对照表中的权重配比
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
根据抽到的奖励档位,抽取号码(主要用于设置抽取豹子号的5,10,15,20,25,30的权重)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
比如上图中如果不设置,色子点数5抽到的概率和其他点数的概率是一样的,可能抽7次T1奖励,就有1次中豹子号5的可能
|
||||||
|
|
||||||
|
由于抽到色子点数和为10,15,20,25的色子点数组合有多种,所以在抽该这四个点数时还需要单独配置相应的中大奖概率(其中豹子号5和30只有一种组合【1,1,1,1,1】和【6,6,6,6,6】,所以不需要配置),其中权重拉到最大10000,那么中奖概率为100%(只要摇到了相应的色子点数和则中奖概率为100%)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 扩展
|
||||||
|
|
||||||
|
- 测试设置的中奖概率,根据如下设置可以测试当前设置权重的中奖概率,该测试数据不记录到真实数据系统中
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
能够准确的反馈抽中点数的统计
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
如果当前中奖概率符合预期,或则测试多组数据选取一组符合预期的导入到当前的配置中
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这里可以详情查询到指定测试记录的详情,
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | saiadmin [ saiadmin快速开发框架 ]
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
namespace plugin\saiadmin\app\controller\system;
|
||||||
|
|
||||||
|
use plugin\saiadmin\app\logic\system\SystemAdminGuideLogic;
|
||||||
|
use plugin\saiadmin\basic\BaseController;
|
||||||
|
use plugin\saiadmin\service\Permission;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台操作指南控制器
|
||||||
|
*/
|
||||||
|
class SystemAdminGuideController extends BaseController
|
||||||
|
{
|
||||||
|
private SystemAdminGuideLogic $guideLogic;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->guideLogic = new SystemAdminGuideLogic();
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取后台操作指南 Markdown 内容
|
||||||
|
*/
|
||||||
|
#[Permission('后台操作指南读取', 'system:admin_guide:index:read')]
|
||||||
|
public function read(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->guideLogic->read();
|
||||||
|
return $this->success($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存后台操作指南 Markdown 内容
|
||||||
|
*/
|
||||||
|
#[Permission('后台操作指南保存', 'system:admin_guide:index:save')]
|
||||||
|
public function save(Request $request): Response
|
||||||
|
{
|
||||||
|
$content = $request->post('content', '');
|
||||||
|
if (! is_string($content)) {
|
||||||
|
return $this->fail('invalid content');
|
||||||
|
}
|
||||||
|
$data = $this->guideLogic->save($content);
|
||||||
|
return $this->success($data, 'save success');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | saiadmin [ saiadmin快速开发框架 ]
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
namespace plugin\saiadmin\app\logic\system;
|
||||||
|
|
||||||
|
use plugin\saiadmin\exception\ApiException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台操作指南逻辑(读写 server/docs/ADMIN_GUIDE.md)
|
||||||
|
*/
|
||||||
|
class SystemAdminGuideLogic
|
||||||
|
{
|
||||||
|
private const GUIDE_FILENAME = 'ADMIN_GUIDE.md';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指南 Markdown 文件绝对路径
|
||||||
|
*/
|
||||||
|
public function getFilePath(): string
|
||||||
|
{
|
||||||
|
return base_path() . DIRECTORY_SEPARATOR . 'docs' . DIRECTORY_SEPARATOR . self::GUIDE_FILENAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取指南内容
|
||||||
|
* @return array{content: string, file_path: string, update_time: string|null}
|
||||||
|
*/
|
||||||
|
public function read(): array
|
||||||
|
{
|
||||||
|
$filePath = $this->getFilePath();
|
||||||
|
if (! is_file($filePath)) {
|
||||||
|
throw new ApiException('admin guide file not found');
|
||||||
|
}
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
if ($content === false) {
|
||||||
|
throw new ApiException('failed to read admin guide file');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'content' => $content,
|
||||||
|
'file_path' => 'docs/' . self::GUIDE_FILENAME,
|
||||||
|
'update_time' => date('Y-m-d H:i:s', filemtime($filePath)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存指南内容到 Markdown 文件
|
||||||
|
* @param string $content
|
||||||
|
* @return array{content: string, file_path: string, update_time: string}
|
||||||
|
*/
|
||||||
|
public function save(string $content): array
|
||||||
|
{
|
||||||
|
$filePath = $this->getFilePath();
|
||||||
|
$dir = dirname($filePath);
|
||||||
|
if (! is_dir($dir) && ! mkdir($dir, 0755, true) && ! is_dir($dir)) {
|
||||||
|
throw new ApiException('failed to create docs directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = file_put_contents($filePath, $content, LOCK_EX);
|
||||||
|
if ($result === false) {
|
||||||
|
throw new ApiException('failed to save admin guide file');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearstatcache(true, $filePath);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'content' => $content,
|
||||||
|
'file_path' => 'docs/' . self::GUIDE_FILENAME,
|
||||||
|
'update_time' => date('Y-m-d H:i:s', filemtime($filePath)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,10 @@ Route::group('/core', function () {
|
|||||||
Route::delete("/logs/deleteOperLog", [\plugin\saiadmin\app\controller\system\SystemLogController::class, 'deleteOperLog']);
|
Route::delete("/logs/deleteOperLog", [\plugin\saiadmin\app\controller\system\SystemLogController::class, 'deleteOperLog']);
|
||||||
fastRoute("email", \plugin\saiadmin\app\controller\system\SystemMailController::class);
|
fastRoute("email", \plugin\saiadmin\app\controller\system\SystemMailController::class);
|
||||||
|
|
||||||
|
// 后台操作指南
|
||||||
|
Route::get("/adminGuide/read", [\plugin\saiadmin\app\controller\system\SystemAdminGuideController::class, 'read']);
|
||||||
|
Route::post("/adminGuide/save", [\plugin\saiadmin\app\controller\system\SystemAdminGuideController::class, 'save']);
|
||||||
|
|
||||||
// 服务管理
|
// 服务管理
|
||||||
Route::get("/server/monitor", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'monitor']);
|
Route::get("/server/monitor", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'monitor']);
|
||||||
Route::get("/server/cache", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'cache']);
|
Route::get("/server/cache", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'cache']);
|
||||||
|
|||||||
BIN
server/public/docs/picture/guide_01.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
server/public/docs/picture/guide_02.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
server/public/docs/picture/guide_03.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
server/public/docs/picture/guide_04.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
server/public/docs/picture/guide_05.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
server/public/docs/picture/guide_06.png
Normal file
|
After Width: | Height: | Size: 671 KiB |
BIN
server/public/docs/picture/guide_07.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
server/public/docs/picture/guide_08.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
server/public/docs/picture/guide_09.png
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
server/public/docs/picture/guide_10.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
server/public/docs/picture/guide_11.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
server/public/docs/picture/guide_12.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
server/public/docs/picture/guide_13.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
server/public/docs/picture/guide_14.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
server/public/docs/picture/guide_15.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
server/public/docs/picture/guide_16.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
server/public/docs/picture/guide_17.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
server/public/docs/picture/guide_18.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
server/public/docs/picture/guide_19.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
server/public/docs/picture/guide_20.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
server/public/docs/picture/guide_21.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
server/public/docs/picture/guide_22.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
server/public/docs/picture/guide_23.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
server/public/docs/picture/guide_24.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
server/public/docs/picture/guide_25.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
server/public/docs/picture/guide_26.png
Normal file
|
After Width: | Height: | Size: 113 KiB |