MineAdmin默认为单租户模式,本次实战采用不分库使用tenant_id来作为数据隔离
数据库查询 使用全局作用域来隔离数据,后台创建的第一个租户默认为管理员。登录时输入租户企业名称 验证租户到期时间 无法删除创始人租户
创建多租户套餐、租户管理
CREATE TABLE `mineadmin`.`tenant_package` (
`package_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`package_name` varchar(30) NOT NULL COMMENT '套餐名称',
`menu_ids` varchar(2000) NULL COMMENT '菜单',
`remark` varchar(255) NULL COMMENT '备注',
`status` char(1) NULL DEFAULT 1 COMMENT '状态',
`created_by` bigint(20) NULL COMMENT '创建人',
`created_at` datetime NULL COMMENT '创建时间',
`updated_by` bigint(20) NULL COMMENT '修改人',
`updated_at` datetime NULL COMMENT '修改时间',
PRIMARY KEY (`package_id`)
) COMMENT = '租户套餐';
CREATE TABLE `mineadmin`.`tenant_info` (
`tenant_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`company_name` varchar(100) NOT NULL COMMENT '公司名称',
`contract_name` varchar(20) NULL COMMENT '联系人',
`contract_phone` varchar(20) NULL COMMENT '联系电话',
`package_id` bigint(20) NOT NULL COMMENT '套餐编号',
`exprie_time` datetime NULL COMMENT '到期时间',
`account_num` int(11) NULL COMMENT '用户数量(-1不限制)',
`status` char(1) NOT NULL DEFAULT 1 COMMENT '租户状态 1正常 2停用',
`remark` varchar(1000) NULL COMMENT '备注',
`user_id` bigint(20) NOT NULL COMMENT '管理员ID',
`created_by` bigint(20) NULL COMMENT '创建人',
`created_at` datetime NULL COMMENT '创建时间',
`updated_by` bigint(20) NULL COMMENT '修改人',
`updated_at` datetime NULL COMMENT '修改时间',
PRIMARY KEY (`tenant_id`)
) COMMENT = '租户信息表';
数据表创建好之后在后台创建Tenant模块后对上面创建的两张表创建CRUD
并且对两个模型进行关联
TenantInfo(package_id) -> hasOne -> TenantPackage(package_id)
TenantPackage(package_id) -> hasMany -> TenantInfo(package_id)
生成完毕后重启Hyperf
修改src/views/tenant/package/index.vue中的套餐选择相关代码
这里主要记录一下菜单选择怎么写
按照MineAdmin文档介绍,字段可以套一个component,菜单编辑时我们就自己定义一个组件
src/views/tenant/package/menuChose.vue
<template>
<a-spin :loading="loading" tip="菜单加载中..." class="w-full">
<div class="w-full">
<a-space class="mt-1.5" size="large">
<a-checkbox @change="handlerExpand">展开/折叠</a-checkbox>
<a-checkbox @change="handlerSelect">全选/全不选</a-checkbox>
<a-checkbox v-model="cancelLinkage" @change="handlerLinkage">关闭父子级联动</a-checkbox>
</a-space>
<div class="tree-container p-2">
<ma-tree-slider ref="tree" :data="menuList" checkable :fieldNames="{ title: 'label', key: 'id' }"
searchPlaceholder="过滤菜单" v-model:checked-keys="selectKeys" :check-strictly="cancelLinkage"
:virtual-list-props="{ height: 300 }" @click="handlerClick" />
</div>
</div>
</a-spin>
</template>
<script setup>
import { inject, onMounted, ref, watch } from 'vue'
import MaTreeSlider from '@cps/ma-treeSlider/index.vue'
import menu from '@/api/system/menu'
import { Message } from '@arco-design/web-vue';
const loading = ref(true)
const menuList = ref([])
const selectKeys = ref([])
const cancelLinkage = ref(false)
const tree = ref()
const form = inject('formModel')
onMounted(async () => {
handlerExpand(false)
handlerSelect(false)
handlerLinkage(false)
await setData()
})
watch(selectKeys,
(val_new, val_old) => {
if(val_new.length > 0 || val_old.length > 0) {
form.value.menu_ids = val_new.map(item => parseInt(item))
}
}
)
const handlerExpand = (value) => {
tree.value.maTree.expandAll(value)
}
const handlerSelect = (value) => {
tree.value.maTree.checkAll(value)
}
const handlerLinkage = (value) => {
cancelLinkage.value = value
}
const handlerClick = (value) => {
const t = tree.value.maTree
const nodes = t.getExpandedNodes().map(item => item.id)
t.expandNode(value, nodes.includes(value[0]) ? false : true)
}
const setData = async () => {
loading.value = true
const menuResponse = await menu.tree({ scope: true })
menuList.value = menuResponse.data
if (form.value.menu_ids && typeof(form.value.menu_ids) == 'string') {
const roleResponse = form.value.menu_ids.split(",")
selectKeys.value = roleResponse.map(item => parseInt(item))
selectKeys.value.length > 0 && handlerLinkage(true)
}
loading.value = false
}
</script>
<style scoped>
.tree-container {
border: 1px solid var(--color-fill-2);
max-height: 350px;
padding-bottom: 8px;
margin-top: 5px;
}
</style>
src/views/tenant/package/index.vue
<template>
<div class="ma-content-block lg:flex justify-between p-4">
<!-- CRUD 组件 -->
<ma-crud :options="options" :columns="columns" ref="crudRef">
<template #status="{ record }">
<a-switch
checked-value="1"
unchecked-value="2"
:default-checked="record.status == 1"
@change="switchStatus($event, record.id, 'status')"
></a-switch>
</template>
</ma-crud>
</div>
</template>
<script setup>
import { ref, reactive, shallowRef } from 'vue'
import tenantPackage from '@/api/tenant/tenantPackage'
import { Message } from '@arco-design/web-vue'
import tool from '@/utils/tool'
import * as common from '@/utils/common'
import menuChose from "./menuChose.vue"
const crudRef = ref()
const switchStatus = (statusValue, id, statusName) => {
tenantPackage.changeStatus({ id, statusName, statusValue }).then( res => {
res.success && Message.success(res.message)
}).catch( e => { console.log(e) } )
}
const options = reactive({
id: 'tenant_package',
rowSelection: {
showCheckedAll: true
},
pk: 'package_id',
operationColumn: true,
operationColumnWidth: 160,
formOption: {
viewType: 'modal',
width: 600
},
api: tenantPackage.getList,
add: {
show: true,
api: tenantPackage.save,
auth: ['tenant:package:save']
},
edit: {
show: true,
api: tenantPackage.update,
auth: ['tenant:package:update']
},
delete: {
show: true,
api: tenantPackage.deletes,
auth: ['tenant:package:delete']
}
})
const columns = reactive([
{
title: "编号",
dataIndex: "package_id",
formType: "input",
addDisplay: false,
editDisplay: false,
hide: true,
commonRules: {
required: true,
message: "请输入编号"
}
},
{
title: "套餐名称",
dataIndex: "package_name",
formType: "input",
search: true,
commonRules: {
required: true,
message: "请输入套餐名称"
}
},
{
title: "菜单",
dataIndex: "menu_ids",
formType: "component",
component: shallowRef(menuChose),
commonRules: {
required: true,
message: "请选择菜单"
},
hide: true
},
{
title: "备注",
dataIndex: "remark",
formType: "textarea",
addDisplay: true,
editDisplay: true
},
{
title: "状态",
dataIndex: "status",
formType: "radio",
search: true,
dict: {
name: "data_status",
props: { label: "title", value: "key"}
},
addDefaultValue : "1",
},
{
title: "创建人",
dataIndex: "created_by",
formType: "input",
addDisplay: false,
editDisplay: false,
hide: true
},
{
title: "创建时间",
dataIndex: "created_at",
formType: "date",
addDisplay: false,
editDisplay: false,
hide: true,
showTime: true
},
{
title: "修改人",
dataIndex: "updated_by",
formType: "input",
addDisplay: false,
editDisplay: false,
hide: true
},
{
title: "修改时间",
dataIndex: "updated_at",
formType: "date",
addDisplay: false,
editDisplay: false,
hide: true,
showTime: true
}
])
</script>
<script> export default { name: 'tenant:package' } </script>
还需要处理一下后端的保存,将menu_ids 存为string类型,需要涉及到Hyperf的获取器和修改器。
app/Tenant/Model/TenantPackage.php
public function getMenuIdsAttribute(string|null $value)
{
if (is_string($value)) return explode(",", $value);
}
public function setMenuIdsAttribute(array|string $value)
{
if (is_array($value)) $this->attributes['menu_ids'] = implode(",", $value);
}
到这里租户套餐就构建完成