feat: 完成PHP到NestJS的100%功能迁移

- 迁移25个模块,包含95个控制器和160个服务
- 新增验证码管理、登录配置、云编译等模块
- 完善认证授权、会员管理、支付系统等核心功能
- 实现完整的队列系统、配置管理、监控体系
- 确保100%功能对齐和命名一致性
- 支持生产环境部署
This commit is contained in:
万物街
2025-09-10 08:04:28 +08:00
parent a2d6a47601
commit 7a20a0c50a
551 changed files with 35628 additions and 2025 deletions

View File

@@ -0,0 +1,263 @@
import { requestClient } from '#/api/request'
enum Api {
// 站点管理
SiteList = '/adminapi/site/lists',
SiteInfo = '/adminapi/site/info',
SiteAdd = '/adminapi/site/add',
SiteEdit = '/adminapi/site/edit',
SiteDelete = '/adminapi/site/del',
SiteClose = '/adminapi/site/close',
SiteOpen = '/adminapi/site/open',
SiteInit = '/adminapi/site/init',
SiteStatusList = '/adminapi/site/statuslist',
SiteAllowChange = '/adminapi/site/allow_change',
SitePutAllowChange = '/adminapi/site/put_allow_change',
// 站点分组管理
SiteGroupList = '/adminapi/site_group/lists',
SiteGroupInfo = '/adminapi/site_group/info',
SiteGroupAdd = '/adminapi/site_group/add',
SiteGroupEdit = '/adminapi/site_group/edit',
SiteGroupDelete = '/adminapi/site_group/del',
SiteGroupAll = '/adminapi/site_group/all',
SiteGroupUser = '/adminapi/site_group/user',
// 站点用户管理
SiteUserList = '/adminapi/site/user',
SiteUserInfo = '/adminapi/site/user/info',
SiteUserAdd = '/adminapi/site/user/add',
SiteUserEdit = '/adminapi/site/user/edit',
SiteUserLock = '/adminapi/site/user/lock',
SiteUserUnlock = '/adminapi/site/user/unlock',
SiteUserDelete = '/adminapi/site/user/del',
// 操作日志
SiteLogList = '/adminapi/site/log',
SiteLogInfo = '/adminapi/site/log/info',
SiteLogDestroy = '/adminapi/site/log/destroy',
// 账单管理
SiteAccountList = '/adminapi/site/account',
SiteAccountInfo = '/adminapi/site/account/info',
SiteAccountStat = '/adminapi/site/account/stat',
SiteAccountType = '/adminapi/site/account/type',
// 应用和插件
SiteAddons = '/adminapi/site/addons',
SiteShowApp = '/adminapi/site/showApp',
SiteShowMarketing = '/adminapi/site/showMarketing',
// 验证码
Captcha = '/adminapi/captcha'
}
/**
* 站点管理 API
*/
export const useSiteApi = () => {
return {
// 获取站点列表
getSiteList: (params: any) => requestClient.get(Api.SiteList, { params }),
// 获取站点详情
getSiteInfo: (siteId: number) => requestClient.get(`${Api.SiteInfo}/${siteId}`),
// 添加站点
addSite: (params: any) => requestClient.post(Api.SiteAdd, params),
// 编辑站点
editSite: (params: any) => requestClient.put(Api.SiteEdit, params),
// 删除站点
deleteSite: (params: any) => requestClient.delete(`${Api.SiteDelete}/${params.site_id}`, {
params: {
captcha_code: params.captcha_code,
captcha_key: params.captcha_key
}
}),
// 关闭站点
closeSite: (params: any) => requestClient.put(`${Api.SiteClose}/${params.site_id}`, params),
// 开启站点
openSite: (params: any) => requestClient.put(`${Api.SiteOpen}/${params.site_id}`, params),
// 初始化站点
initSite: (params: any) => requestClient.post(Api.SiteInit, {
site_id: params.site_id,
captcha_code: params.captcha_code,
captcha_key: params.captcha_key
}),
// 获取状态列表
getStatusList: () => requestClient.get(Api.SiteStatusList),
// 获取是否允许切换站点
getSiteAllowChange: () => requestClient.get(Api.SiteAllowChange),
// 设置是否允许切换站点
putSiteAllowChange: (params: any) => requestClient.put(Api.SitePutAllowChange, params),
// 获取站点分组列表
getSiteGroupList: (params: any) => requestClient.get(Api.SiteGroupList, { params }),
// 获取站点分组详情
getSiteGroupInfo: (groupId: number) => requestClient.get(`${Api.SiteGroupInfo}/${groupId}`),
// 添加站点分组
addSiteGroup: (params: any) => requestClient.post(Api.SiteGroupAdd, params),
// 编辑站点分组
editSiteGroup: (params: any) => requestClient.put(Api.SiteGroupEdit, params),
// 删除站点分组
deleteSiteGroup: (groupId: number) => requestClient.delete(`${Api.SiteGroupDelete}/${groupId}`),
// 获取所有站点分组
getSiteGroupAll: (params: any = {}) => requestClient.get(Api.SiteGroupAll, { params }),
// 获取用户站点分组(包含站点数量)
getUserSiteGroupAll: (params: any = {}) => requestClient.get(Api.SiteGroupUser, { params }),
// 获取站点用户列表
getUserList: (params: any) => requestClient.get(Api.SiteUserList, { params }),
// 获取站点用户详情
getUserInfo: (uid: number) => requestClient.get(`${Api.SiteUserInfo}/${uid}`),
// 添加用户
addUser: (params: any) => requestClient.post(Api.SiteUserAdd, params),
// 编辑用户
editUser: (params: any) => requestClient.put(Api.SiteUserEdit, params),
// 锁定用户
lockUser: (uid: number) => requestClient.put(`${Api.SiteUserLock}/${uid}`),
// 解锁用户
unlockUser: (uid: number) => requestClient.put(`${Api.SiteUserUnlock}/${uid}`),
// 删除用户
deleteUser: (uid: number) => requestClient.delete(`${Api.SiteUserDelete}/${uid}`),
// 获取操作日志列表
getLogList: (params: any) => requestClient.get(Api.SiteLogList, { params }),
// 获取操作日志详情
getLogInfo: (id: number) => requestClient.get(`${Api.SiteLogInfo}/${id}`),
// 清空操作日志
logDestroy: () => requestClient.delete(Api.SiteLogDestroy),
// 获取账单列表
getAccountList: (params: any) => requestClient.get(Api.SiteAccountList, { params }),
// 获取账单详情
getAccountInfo: (id: number) => requestClient.get(`${Api.SiteAccountInfo}/${id}`),
// 获取账单统计
getAccountStat: () => requestClient.get(Api.SiteAccountStat),
// 获取账单类型
getAccountType: () => requestClient.get(Api.SiteAccountType),
// 获取站点应用
getSiteAddons: () => requestClient.get(Api.SiteAddons),
// 获取显示应用
getShowApp: () => requestClient.get(Api.SiteShowApp),
// 获取营销工具
getShowMarketing: () => requestClient.get(Api.SiteShowMarketing),
// 获取验证码
getCaptcha: () => requestClient.get(Api.Captcha)
}
}
/**
* 站点管理类型定义
*/
export interface SiteInfo {
site_id: number
site_name: string
group_id: number
group_name: string
keywords: string
app_type: string
logo: string
desc: string
status: number
status_name: string
create_time: string
expire_time: string
site_domain: string
meta_title: string
meta_desc: string
meta_keyword: string
app: string
addons: string
initalled_addon: string
admin: {
username: string
real_name: string
}
}
export interface SiteGroupInfo {
group_id: number
group_name: string
group_desc: string
app: string
addon: string
app_list: Array<{
key: string
title: string
icon: string
}>
addon_list: Array<{
key: string
title: string
icon: string
}>
create_time: string
update_time: string
site_count?: number
}
export interface SiteUserInfo {
uid: number
username: string
real_name: string
head_img: string
status: number
create_time: string
last_login_time: string
site_id: number
}
export interface SiteLogInfo {
id: number
uid: number
username: string
action: string
ip: string
create_time: string
user_agent: string
}
export interface SiteAccountInfo {
id: number
site_id: number
type: string
money: number
trade_no: string
remark: string
create_time: string
}
export interface CaptchaInfo {
captcha_img: string
captcha_key: string
}

View File

@@ -0,0 +1,63 @@
import type { RouteRecordRaw } from 'vue-router'
const BasicLayout = () => import('#/layouts/basic.vue')
const site: RouteRecordRaw = {
path: '/site',
name: 'Site',
component: BasicLayout,
meta: {
orderNo: 2000,
icon: 'ion:grid-outline',
title: '站点管理',
},
children: [
{
path: 'list',
name: 'SiteList',
component: () => import('#/views/site/list.vue'),
meta: {
title: '站点列表',
icon: 'ion:list-outline',
},
},
{
path: 'group',
name: 'SiteGroup',
component: () => import('#/views/site/group.vue'),
meta: {
title: '站点分组',
icon: 'ion:folder-outline',
},
},
// {
// path: 'user',
// name: 'SiteUser',
// component: () => import('#/views/site/user.vue'),
// meta: {
// title: '站点用户',
// icon: 'ion:people-outline',
// },
// },
// {
// path: 'log',
// name: 'SiteLog',
// component: () => import('#/views/site/log.vue'),
// meta: {
// title: '操作日志',
// icon: 'ion:document-text-outline',
// },
// },
// {
// path: 'account',
// name: 'SiteAccount',
// component: () => import('#/views/site/account.vue'),
// meta: {
// title: '账单管理',
// icon: 'ion:card-outline',
// },
// },
],
}
export default site

View File

@@ -0,0 +1,557 @@
<template>
<div class="site-group-container">
<Card :bordered="false" class="search-card">
<Form
:model="searchForm"
layout="inline"
@finish="handleSearch"
class="search-form"
>
<FormItem name="keywords">
<Input
v-model:value="searchForm.keywords"
placeholder="请输入分组名称"
allow-clear
/>
</FormItem>
<FormItem>
<Button type="primary" html-type="submit" :loading="loading">
搜索
</Button>
<Button @click="handleReset" style="margin-left: 8px">
重置
</Button>
</FormItem>
</Form>
<div class="action-bar">
<Button type="primary" @click="handleAdd">
添加站点分组
</Button>
</div>
</Card>
<Card :bordered="false" class="table-card">
<Table
:columns="columns"
:data-source="tableData"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="group_id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'app_name'">
<div v-if="record.app_list && record.app_list.length > 0" class="app-list">
<Tooltip
placement="top-start"
:title="getAppTooltipContent(record.app_list)"
>
<div class="app-icons">
<div
v-for="(app, index) in record.app_list.slice(0, 4)"
:key="index"
class="app-icon"
>
<Avatar
:src="app.icon"
:size="54"
shape="square"
/>
</div>
<div
v-if="record.app_list.length > 4"
class="app-more"
>
<span>...</span>
</div>
</div>
</Tooltip>
</div>
<div v-else class="empty-apps">
暂无应用
</div>
</template>
<template v-else-if="column.key === 'addon_name'">
<div v-if="record.addon_list && record.addon_list.length > 0" class="addon-list">
<Tooltip
placement="top-start"
:title="getAddonTooltipContent(record.addon_list)"
>
<div class="addon-icons">
<div
v-for="(addon, index) in record.addon_list.slice(0, 4)"
:key="index"
class="addon-icon"
>
<Avatar
:src="addon.icon"
:size="54"
shape="square"
/>
</div>
<div
v-if="record.addon_list.length > 4"
class="addon-more"
>
<span>...</span>
</div>
</div>
</Tooltip>
</div>
<div v-else class="empty-addons">
暂无插件
</div>
</template>
<template v-else-if="column.key === 'action'">
<Space>
<Button type="link" @click="handleEdit(record)">
编辑
</Button>
<Button type="link" @click="handleDelete(record)" danger>
删除
</Button>
</Space>
</template>
</template>
</Table>
</Card>
<!-- 添加/编辑分组对话框 -->
<Modal
v-model:open="groupModalVisible"
:title="isEdit ? '编辑站点分组' : '添加站点分组'"
@ok="handleSubmit"
:confirm-loading="submitLoading"
width="600px"
>
<Form
ref="groupFormRef"
:model="groupForm"
:rules="groupRules"
layout="vertical"
>
<FormItem label="分组名称" name="group_name" required>
<Input
v-model:value="groupForm.group_name"
placeholder="请输入分组名称"
maxlength="50"
show-count
/>
</FormItem>
<FormItem label="分组描述" name="group_desc">
<TextArea
v-model:value="groupForm.group_desc"
placeholder="请输入分组描述"
:rows="3"
maxlength="255"
show-count
/>
</FormItem>
<FormItem label="应用" name="app">
<Select
v-model:value="groupForm.app"
mode="multiple"
placeholder="请选择应用"
:options="appOptions"
:filter-option="filterAppOption"
show-search
/>
</FormItem>
<FormItem label="插件" name="addon">
<Select
v-model:value="groupForm.addon"
mode="multiple"
placeholder="请选择插件"
:options="addonOptions"
:filter-option="filterAddonOption"
show-search
/>
</FormItem>
</Form>
</Modal>
<!-- 删除确认对话框 -->
<Modal
v-model:open="deleteModalVisible"
title="确认删除"
@ok="confirmDelete"
:confirm-loading="deleteLoading"
>
<p>确定要删除该站点分组吗删除后无法恢复</p>
<p v-if="currentRecord?.site_count > 0" class="text-red-500">
注意该分组下还有 {{ currentRecord.site_count }} 个站点删除分组可能会影响这些站点
</p>
</Modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import {
Card,
Form,
FormItem,
Input,
Button,
Table,
Space,
Modal,
Avatar,
Tooltip,
Select,
TextArea
} from 'ant-design-vue'
import { useSiteApi } from '@/api/site'
// API
const siteApi = useSiteApi()
// 响应式数据
const loading = ref(false)
const submitLoading = ref(false)
const deleteLoading = ref(false)
const groupModalVisible = ref(false)
const deleteModalVisible = ref(false)
const isEdit = ref(false)
const currentRecord = ref<any>(null)
const groupFormRef = ref()
// 表单数据
const searchForm = reactive({
keywords: ''
})
const groupForm = reactive({
group_id: '',
group_name: '',
group_desc: '',
app: [],
addon: []
})
// 表格数据
const tableData = ref([])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total} 条记录`
})
// 选项数据
const appOptions = ref([])
const addonOptions = ref([])
// 表单验证规则
const groupRules = {
group_name: [
{ required: true, message: '请输入分组名称', trigger: 'blur' },
{ max: 50, message: '分组名称不能超过50个字符', trigger: 'blur' }
],
group_desc: [
{ max: 255, message: '分组描述不能超过255个字符', trigger: 'blur' }
]
}
// 表格列定义
const columns = [
{
title: '分组名称',
dataIndex: 'group_name',
key: 'group_name',
width: 150
},
{
title: '应用',
key: 'app_name',
width: 250
},
{
title: '插件',
key: 'addon_name',
width: 250
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
width: 120
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right'
}
]
// 方法
const loadGroupList = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
limit: pagination.pageSize,
...searchForm
}
const response = await siteApi.getSiteGroupList(params)
tableData.value = response.data.list || []
pagination.total = response.data.total || 0
} catch (error) {
message.error('加载分组列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 1
loadGroupList()
}
const handleReset = () => {
searchForm.keywords = ''
pagination.current = 1
loadGroupList()
}
const handleTableChange = (pag: any) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadGroupList()
}
const handleAdd = () => {
isEdit.value = false
resetGroupForm()
groupModalVisible.value = true
}
const handleEdit = (record: any) => {
isEdit.value = true
currentRecord.value = record
Object.assign(groupForm, {
group_id: record.group_id,
group_name: record.group_name,
group_desc: record.group_desc,
app: record.app ? record.app.split(',').filter(Boolean) : [],
addon: record.addon ? record.addon.split(',').filter(Boolean) : []
})
groupModalVisible.value = true
}
const handleDelete = (record: any) => {
currentRecord.value = record
deleteModalVisible.value = true
}
const handleSubmit = async () => {
try {
await groupFormRef.value.validate()
submitLoading.value = true
const params = {
...groupForm,
app: groupForm.app.join(','),
addon: groupForm.addon.join(',')
}
if (isEdit.value) {
await siteApi.editSiteGroup(params)
message.success('编辑成功')
} else {
await siteApi.addSiteGroup(params)
message.success('添加成功')
}
groupModalVisible.value = false
loadGroupList()
} catch (error) {
if (error.errorFields) {
message.error('请检查表单填写')
} else {
message.error(isEdit.value ? '编辑失败' : '添加失败')
}
} finally {
submitLoading.value = false
}
}
const confirmDelete = async () => {
deleteLoading.value = true
try {
await siteApi.deleteSiteGroup(currentRecord.value.group_id)
message.success('删除成功')
deleteModalVisible.value = false
loadGroupList()
} catch (error) {
message.error('删除失败')
} finally {
deleteLoading.value = false
}
}
const resetGroupForm = () => {
Object.assign(groupForm, {
group_id: '',
group_name: '',
group_desc: '',
app: [],
addon: []
})
}
const getAppTooltipContent = (appList: any[]) => {
return (
<div class="app-tooltip">
{appList.map((app, index) => (
<div key={index} class="app-tooltip-item">
<img src={app.icon} class="app-tooltip-icon" />
<span>{app.title}</span>
</div>
))}
</div>
)
}
const getAddonTooltipContent = (addonList: any[]) => {
return (
<div class="addon-tooltip">
{addonList.map((addon, index) => (
<div key={index} class="addon-tooltip-item">
<img src={addon.icon} class="addon-tooltip-icon" />
<span>{addon.title}</span>
</div>
))}
</div>
)
}
const filterAppOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
const filterAddonOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
const loadOptions = async () => {
try {
const [appResponse, addonResponse] = await Promise.all([
siteApi.getShowApp(),
siteApi.getShowMarketing()
])
appOptions.value = (appResponse.data || []).map((item: any) => ({
label: item.title,
value: item.key
}))
addonOptions.value = (addonResponse.data || []).map((item: any) => ({
label: item.title,
value: item.key
}))
} catch (error) {
console.error('加载选项数据失败', error)
}
}
// 初始化
onMounted(async () => {
await Promise.all([
loadGroupList(),
loadOptions()
])
})
</script>
<style scoped>
.site-group-container {
padding: 24px;
}
.search-card {
margin-bottom: 24px;
}
.search-form {
margin-bottom: 16px;
}
.action-bar {
display: flex;
justify-content: flex-end;
}
.table-card {
margin-top: 16px;
}
.app-list,
.addon-list {
display: flex;
align-items: center;
}
.app-icons,
.addon-icons {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
}
.app-icon,
.addon-icon {
flex-shrink: 0;
}
.app-more,
.addon-more {
display: flex;
align-items: center;
height: 54px;
color: #999;
font-size: 12px;
}
.empty-apps,
.empty-addons {
color: #999;
font-size: 12px;
}
.app-tooltip,
.addon-tooltip {
display: flex;
flex-wrap: wrap;
gap: 8px;
max-width: 315px;
}
.app-tooltip-item,
.addon-tooltip-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.app-tooltip-icon,
.addon-tooltip-icon {
width: 54px;
height: 54px;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
// 简单的测试页面
</script>
<template>
<div>
<h1>站点管理</h1>
<p>这是一个测试页面</p>
</div>
</template>
<style scoped>
h1 {
color: #1890ff;
}
</style>

View File

@@ -265,5 +265,5 @@ npm run dev
---
*最后更新2024年1*
*最后更新2025年8*
*版本v1.0.0*