MineAdmin系统单租户扩展多租户实战1

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

到这里租户套餐就构建完成 

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值