cgp 4 дней назад
Родитель
Сommit
3154d9836c

+ 43 - 0
adminui/src/api/system/menu.js

@@ -141,3 +141,46 @@ export function getTenantComMenu(menuId) {
     method: 'get'
   })
 }
+
+// 查询医生菜单模板列表
+export function tenantDoctorMenu(query) {
+  return request({
+    url: '/tenant/tenant/tenantDoctorMenu/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询医生菜单详细
+export function getTenantDoctorMenu(menuId) {
+  return request({
+    url: '/tenant/tenant/getTenantDoctorMenu/' + menuId,
+    method: 'get'
+  })
+}
+
+// 新增医生菜单
+export function addTenantDoctorMenu(data) {
+  return request({
+    url: '/tenant/tenant/addTenantDoctorMenu',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改医生菜单
+export function updateTenantDoctorMenu(data) {
+  return request({
+    url: '/tenant/tenant/updateTenantDoctorMenu',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除医生菜单
+export function delTenantDoctorMenu(menuId) {
+  return request({
+    url: '/tenant/tenant/delTenantDoctorMenu/' + menuId,
+    method: 'delete'
+  })
+}

+ 2 - 1
adminui/src/views/admin/sysCompany/index.vue

@@ -71,6 +71,7 @@
             <el-button size="mini" type="text" style="color:#722ed1" icon="el-icon-menu" @click="handleEditMenu(scope.row, 'sys')">管理端菜单</el-button>
             <el-button size="mini" type="text" style="color:#e6a23c" icon="el-icon-price-tag" @click="handleModulePricing(scope.row)">模块定价</el-button>
             <el-button size="mini" type="text" style="color:#13c2c2" icon="el-icon-sell" @click="handleEditMenu(scope.row, 'com')">销售菜单</el-button>
+            <el-button size="mini" type="text" style="color:#52c41a" icon="el-icon-menu" @click="handleEditMenu(scope.row, 'doctor')">医生菜单</el-button>
             <el-button
               v-if="scope.row.status == 1"
               size="mini" type="text" style="color:#fa8c16"
@@ -1018,7 +1019,7 @@ export default {
     handleEditMenu(row, flag) {
       this.menuDialog = {
         visible: true,
-        title: (flag === 'sys' ? '编辑管理端菜单 - ' : '编辑销售菜单 - ') + row.tenantName,
+        title: (flag === 'sys' ? '编辑管理端菜单 - ' : flag === 'doctor' ? '编辑医生菜单 - ' : '编辑销售菜单 - ') + row.tenantName,
         flag: flag,
         companyId: row.id,
         companyName: row.tenantName,

+ 24 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/CompanyAdminController.java

@@ -9,6 +9,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysMenu;
 import com.fs.common.core.domain.entity.TenantCompanyMenu;
+import com.fs.common.core.domain.entity.TenantDoctorMenu;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.billing.service.BillingServices;
@@ -363,6 +364,11 @@ public class CompanyAdminController extends BaseController {
                 tenantDataSourceManager.switchTenant(tenantInfo);
                 return tenantInfoService.menuChange(flag, sysMenus, null);
             }
+            if ("doctor".equals(flag)) {
+                List<TenantDoctorMenu> doctorMenus = tenantInfoMapper.selectDoctorMenuList(new TenantDoctorMenu());
+                tenantDataSourceManager.switchTenant(tenantInfo);
+                return tenantInfoService.menuChangeDoctor(doctorMenus);
+            }
 
             List<TenantCompanyMenu> companyMenus = tenantInfoMapper.selectCompanyMenuList(new TenantCompanyMenu());
             tenantDataSourceManager.switchTenant(tenantInfo);
@@ -401,6 +407,16 @@ public class CompanyAdminController extends BaseController {
             return tenantContextHelper.executeInTenant(tenantInfo,
                     () -> tenantInfoService.menuAssignReplace(finalSelected, flag, assignSysMenu, null));
         }
+        if ("doctor".equals(flag)) {
+            List<TenantDoctorMenu> allTemplateMenus = tenantContextHelper.executeInMaster(
+                    () -> tenantInfoMapper.selectDoctorMenuList(new TenantDoctorMenu()));
+            List<Long> expandedSelected = tenantInfoService.expandDoctorMenuIdsWithAncestors(selected, allTemplateMenus);
+            List<TenantDoctorMenu> assignDoctorMenu = tenantContextHelper.executeInMaster(
+                    () -> loadMasterDoctorMenus(expandedSelected));
+            List<Long> finalSelected = expandedSelected;
+            return tenantContextHelper.executeInTenant(tenantInfo,
+                    () -> tenantInfoService.menuAssignReplaceDoctor(finalSelected, assignDoctorMenu));
+        }
         List<TenantCompanyMenu> allTemplateMenus = tenantContextHelper.executeInMaster(
                 () -> tenantInfoMapper.selectCompanyMenuList(new TenantCompanyMenu()));
         List<Long> expandedSelected = tenantInfoService.expandComMenuIdsWithAncestors(selected, allTemplateMenus);
@@ -427,6 +443,14 @@ public class CompanyAdminController extends BaseController {
         return tenantInfoMapper.getTenComMenuByIds(selected);
     }
 
+    /** 主库 tenant_doctor_menu 模板 */
+    private List<TenantDoctorMenu> loadMasterDoctorMenus(List<Long> selected) {
+        if (selected == null || selected.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return tenantInfoMapper.getTenDoctorMenuByIds(selected);
+    }
+
     private List<Long> toLongList(Object raw) {
         if (raw == null || !(raw instanceof List)) {
             return new ArrayList<>();

+ 158 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/tenant/TenantInfoController.java

@@ -8,6 +8,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysMenu;
 import com.fs.common.core.domain.entity.TenantCompanyMenu;
+import com.fs.common.core.domain.entity.TenantDoctorMenu;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.enums.DataSourceType;
@@ -161,6 +162,11 @@ public class TenantInfoController extends BaseController
             tenantDataSourceManager.switchTenant(tenantInfo);
             return tenantInfoService.menuChange(flag, sysMenus,null);
         }
+        if ("doctor".equals(flag)){
+            List<TenantDoctorMenu> doctorMenus = tenantInfoMapper.selectDoctorMenuList(new TenantDoctorMenu());
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            return tenantInfoService.menuChangeDoctor(doctorMenus);
+        }
 
         List<TenantCompanyMenu> companyMenus = tenantInfoMapper.selectCompanyMenuList(new TenantCompanyMenu());
         tenantDataSourceManager.switchTenant(tenantInfo);
@@ -189,6 +195,13 @@ public class TenantInfoController extends BaseController
             tenantDataSourceManager.switchTenant(tenantInfo);
             return tenantInfoService.menuEdit(expandedSelected, unSelected, menuDto.getFlag(), addSysMenu, null);
         }
+        if ("doctor".equals(menuDto.getFlag())) {
+            List<TenantDoctorMenu> allTemplateMenus = tenantInfoMapper.selectDoctorMenuList(new TenantDoctorMenu());
+            List<Long> expandedSelected = tenantInfoService.expandDoctorMenuIdsWithAncestors(selected, allTemplateMenus);
+            List<TenantDoctorMenu> addDoctorMenu = getAddDoctorMenu(tenantInfo, expandedSelected);
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            return tenantInfoService.menuEditDoctor(expandedSelected, unSelected, addDoctorMenu);
+        }
 
         List<TenantCompanyMenu> allTemplateMenus = tenantInfoMapper.selectCompanyMenuList(new TenantCompanyMenu());
         List<Long> expandedSelected = tenantInfoService.expandComMenuIdsWithAncestors(selected, allTemplateMenus);
@@ -282,6 +295,28 @@ public class TenantInfoController extends BaseController
         return new ArrayList<>();
     }
 
+    /**
+     * 获取需要更新的医生菜单
+     */
+    private List<TenantDoctorMenu> getAddDoctorMenu(TenantInfo tenantInfo, List<Long> selected){
+        // 切换到租户库
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        // 查询租户库里已经存在的menuId
+        List<Long> existIds = tenantInfoMapper.selectTenantDbDoctorMenuIds();
+        // 不存在的menuId(就是要新增的)
+        List<Long> needAddIds = selected.stream()
+                .filter(id -> !existIds.contains(id))
+                .collect(Collectors.toList());
+        // 去总库查询详细的菜单详情
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (!CollectionUtils.isEmpty(needAddIds)) {
+            List<TenantDoctorMenu> addMenuList = tenantInfoMapper.getTenDoctorMenuByIds(needAddIds);
+            return addMenuList;
+        }
+
+        return new ArrayList<>();
+    }
+
     /**
      * 获取租户总后台菜单列表
      */
@@ -453,6 +488,87 @@ public class TenantInfoController extends BaseController
         return toAjax(result);
     }
 
+    /**
+     * 获取租户医生菜单列表(标准模板)
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:list')")
+    @GetMapping("/tenantDoctorMenu/list")
+    public AjaxResult doctorMenuList(TenantDoctorMenu menu)
+    {
+        List<TenantDoctorMenu> menus = tenantInfoService.selectDoctorMenuList(menu, getUserId());
+        return AjaxResult.success(menus);
+    }
+
+    /**
+     * 根据菜单编号获取医生菜单详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:query')")
+    @GetMapping(value = "/getTenantDoctorMenu/{menuId}")
+    public AjaxResult getTenantDoctorMenu(@PathVariable Long menuId)
+    {
+        return AjaxResult.success(tenantInfoService.getTenantDoctorMenu(menuId));
+    }
+
+    /**
+     * 新增医生菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:add')")
+    @Log(title = "菜单管理", businessType = BusinessType.INSERT)
+    @PostMapping("/addTenantDoctorMenu")
+    public AjaxResult addTenantDoctorMenu(@Validated @RequestBody TenantDoctorMenu menu)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(tenantInfoService.checkDoctorMenuNameUnique(menu)))
+        {
+            return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        }
+        menu.setCreateBy(getUsername());
+        return toAjax(tenantInfoService.insertDoctorMenu(menu));
+    }
+
+    /**
+     * 修改医生菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:edit')")
+    @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateTenantDoctorMenu")
+    public AjaxResult updateTenantDoctorMenu(@Validated @RequestBody TenantDoctorMenu menu)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(tenantInfoService.checkDoctorMenuNameUnique(menu)))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        }
+        else if (menu.getMenuId().equals(menu.getParentId()))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
+        }
+        menu.setUpdateBy(getUsername());
+        int result = tenantInfoService.updateDoctorMenu(menu);
+        if (result > 0) {
+            TenantDoctorMenu fullMenu = tenantInfoService.getTenantDoctorMenu(menu.getMenuId());
+            CompletableFuture.runAsync(() -> syncDoctorMenuUpdateToTenants(fullMenu));
+        }
+        return toAjax(result);
+    }
+
+    /**
+     * 删除医生菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:remove')")
+    @Log(title = "菜单管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/delTenantDoctorMenu/{menuId}")
+    public AjaxResult delTenantDoctorMenu(@PathVariable("menuId") Long menuId)
+    {
+        if (tenantInfoService.hasChildByDoctorMenuId(menuId))
+        {
+            return AjaxResult.error("存在子菜单,不允许删除");
+        }
+        int result = tenantInfoService.deleteDoctorMenuById(menuId);
+        if (result > 0) {
+            CompletableFuture.runAsync(() -> syncDoctorMenuDeleteToTenants(menuId));
+        }
+        return toAjax(result);
+    }
+
     // ========== 菜单模板变更 → 异步同步到所有启用租户库 ==========
 
     /**
@@ -539,6 +655,48 @@ public class TenantInfoController extends BaseController
         }
     }
 
+    /**
+     * 同步 doctor 菜单修改到所有 status=1 的租户库(仅已存在该菜单的租户执行 upsert)
+     */
+    private void syncDoctorMenuUpdateToTenants(TenantDoctorMenu menu) {
+        List<TenantInfo> tenants = getActiveTenantsForSync();
+        for (TenantInfo tenant : tenants) {
+            try {
+                tenantContextHelper.executeInTenant(tenant, () -> {
+                    int count = tenantInfoMapper.countTenantDoctorMenuById(menu.getMenuId());
+                    if (count > 0) {
+                        tenantInfoMapper.upsertDoctorMenu(Collections.singletonList(menu));
+                    }
+                    return null;
+                });
+            } catch (Exception e) {
+                log.error("同步医生菜单修改失败: tenant={}, menuId={}", tenant.getTenantCode(), menu.getMenuId(), e);
+            }
+        }
+    }
+
+    /**
+     * 同步 doctor 菜单删除到所有 status=1 的租户库(仅已存在该菜单的租户执行删除)
+     */
+    private void syncDoctorMenuDeleteToTenants(Long menuId) {
+        List<TenantInfo> tenants = getActiveTenantsForSync();
+        for (TenantInfo tenant : tenants) {
+            try {
+                tenantContextHelper.executeInTenant(tenant, () -> {
+                    int count = tenantInfoMapper.countTenantDoctorMenuById(menuId);
+                    if (count > 0) {
+                        List<Long> menuIds = Collections.singletonList(menuId);
+                        tenantInfoMapper.deleteDoctorRoleMenuByMenuIds(menuIds);
+                        tenantInfoMapper.deleteTenantDoctorMenuByIds(menuIds);
+                    }
+                    return null;
+                });
+            } catch (Exception e) {
+                log.error("同步医生菜单删除失败: tenant={}, menuId={}", tenant.getTenantCode(), menuId, e);
+            }
+        }
+    }
+
     /**
      * 查询所有 status=1(启用)的租户,在主库执行
      */

+ 115 - 75
java/fs-doctor-app/src/main/java/com/fs/app/controller/DoctorController.java

@@ -12,6 +12,12 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.sign.Md5Utils;
 import com.fs.config.saas.ProjectConfig;
+import com.fs.common.enums.DataSourceType;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import com.fs.doctor.service.FsDoctorPermissionService;
 import com.fs.his.domain.*;
 import com.fs.his.dto.FsExtractDTO;
 import com.fs.his.enums.FsExtractTypeEnum;
@@ -73,6 +79,12 @@ public class DoctorController extends  AppBaseController {
     private SmsService smsService;
     @Autowired
     private SysConfigMapper sysConfigMapper;
+    @Autowired
+    private TenantInfoService tenantInfoService;
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+    @Autowired
+    private FsDoctorPermissionService fsDoctorPermissionService;
     /**
      * 查询配置文件
      *
@@ -83,43 +95,14 @@ public class DoctorController extends  AppBaseController {
         return projectConfig != null ? projectConfig.getOpenIM() : null;
     }
 
-    @ApiOperation("登录")
+    /**
+     * 医生登录入口 —— 小程序 / App 端。
+     * <p>不校验验证码、不返回 Vue 菜单树(routers);小程序 tabBar 由前端固定,功能显隐靠返回的 permissions 控制。</p>
+     */
+    @ApiOperation("医生登录(小程序/App 端)")
     @PostMapping("/login")
-    public R login(@Validated  @RequestBody DoctorLoginParam param) {
-        FsDoctor doctor=doctorService.selectFsDoctorByAccount(param.getAccount());
-        if(doctor==null||!doctor.getStatus().equals(1)){
-            return R.error("帐号不存在或已停用");
-        }
-        else{
-            if(StringUtils.isNotEmpty(param.getJpushId())){
-                FsDoctor doctorMap=new FsDoctor();
-                doctorMap.setDoctorId(doctor.getDoctorId());
-                doctorMap.setJpushId(param.getJpushId());
-                doctorService.updateFsDoctor(doctorMap);
-            }
-
-            if(!doctor.getDoctorType().equals(param.getType())){
-                return R.error("非法操作");
-            }
-            if(!Md5Utils.hash(param.getPassword()).equals(doctor.getPassword())){
-                return R.error("密码不正确");
-            }
-            String token = jwtUtils.generateToken(doctor.getDoctorId());
-            redisCache.setCacheObject("doctorToken:"+doctor.getDoctorId(),token,604800, TimeUnit.SECONDS);
-            Map<String,Object> map=new HashMap<>();
-            map.put("token",token);
-            map.put("doctor",doctor);
-            if(doctor.getDeptId()!=null){
-                FsDepartment department=departmentService.selectFsDepartmentByDeptId(doctor.getDeptId());
-                if(department!=null){
-                    map.put("deptName",department.getDeptName());
-                }
-                else{
-                    map.put("deptName","");
-                }
-            }
-            return R.ok(map);
-        }
+    public R login(@Validated @RequestBody DoctorLoginParam param) {
+        return doLogin(param, false, false);
     }
     @ApiOperation("校验医生是否注册新的im")
     @PostMapping("/accountCheck")
@@ -188,51 +171,67 @@ public class DoctorController extends  AppBaseController {
             return R.error("获取管理员token失败");
         }
     }
-    @ApiOperation("登录")
+    /**
+     * 医生登录入口 —— Web 管理端。
+     * <p>开启验证码,并返回 Vue 菜单树(routers),供前端动态路由(router.addRoutes)渲染侧边栏。</p>
+     */
+    @ApiOperation("医生登录(Web 管理端)")
     @PostMapping("/loginByWeb")
     public R loginByWeb(@Validated @RequestBody DoctorLoginParam param) {
-        FsDoctor doctor=doctorService.selectFsDoctorByAccount(param.getAccount());
-        if(doctor==null||!doctor.getStatus().equals(1)){
+        return doLogin(param, true, true);
+    }
+
+    /**
+     * 医生登录公共逻辑(Web 端与小程序/App 端共用)。
+     *
+     * @param param          登录参数(含 tenantCode,用于切租户库)
+     * @param checkCaptcha   是否校验验证码(Web 端开启;小程序/App 端关闭)
+     * @param includeRouters 是否返回 Vue 菜单树(Web 端动态路由需要;小程序/App 端不需要)
+     * @return 登录结果
+     */
+    private R doLogin(DoctorLoginParam param, boolean checkCaptcha, boolean includeRouters) {
+        String tenantError = checkAndSwitchTenant(param.getTenantCode());
+        if (tenantError != null) {
+            return R.error(tenantError);
+        }
+        FsDoctor doctor = doctorService.selectFsDoctorByAccount(param.getAccount());
+        if (doctor == null || !doctor.getStatus().equals(1)) {
             return R.error("帐号不存在或已停用");
         }
-        else{
-            if(StringUtils.isNotEmpty(param.getJpushId())){
-                FsDoctor doctorMap=new FsDoctor();
-                doctorMap.setDoctorId(doctor.getDoctorId());
-                doctorMap.setJpushId(param.getJpushId());
-                doctorService.updateFsDoctor(doctorMap);
-            }
-            if(!doctor.getDoctorType().equals(param.getType())){
-                return R.error("非法操作");
-            }
-            if(!Md5Utils.hash(param.getPassword()).equals(doctor.getPassword())){
-                return R.error("密码不正确");
-            }
-            boolean captchaOnOff = configService.selectCaptchaOnOff();
-            // 验证码开关
-            if (captchaOnOff)
-            {
-                String msg =  validateCaptcha(param.getAccount(), param.getCode(), param.getUuid());
-                if(!msg.equals("验证成功")){
-                    return R.error(msg);
-                };
-            }
-            String token = jwtUtils.generateToken(doctor.getDoctorId());
-            redisCache.setCacheObject("doctorToken:"+doctor.getDoctorId(),token,604800, TimeUnit.SECONDS);
-            Map<String,Object> map=new HashMap<>();
-            map.put("token",token);
-            map.put("doctor",doctor);
-            if(doctor.getDeptId()!=null){
-                FsDepartment department=departmentService.selectFsDepartmentByDeptId(doctor.getDeptId());
-                if(department!=null){
-                    map.put("deptName",department.getDeptName());
-                }
-                else{
-                    map.put("deptName","");
-                }
+        if (StringUtils.isNotEmpty(param.getJpushId())) {
+            FsDoctor doctorMap = new FsDoctor();
+            doctorMap.setDoctorId(doctor.getDoctorId());
+            doctorMap.setJpushId(param.getJpushId());
+            doctorService.updateFsDoctor(doctorMap);
+        }
+        if (!doctor.getDoctorType().equals(param.getType())) {
+            return R.error("非法操作");
+        }
+        if (!Md5Utils.hash(param.getPassword()).equals(doctor.getPassword())) {
+            return R.error("密码不正确");
+        }
+        if (checkCaptcha && configService.selectCaptchaOnOff()) {
+            String msg = validateCaptcha(param.getAccount(), param.getCode(), param.getUuid());
+            if (!"验证成功".equals(msg)) {
+                return R.error(msg);
             }
-            return R.ok(map);
         }
+        String token = jwtUtils.generateToken(doctor.getDoctorId());
+        redisCache.setCacheObject("doctorToken:" + doctor.getDoctorId(), token, 604800, TimeUnit.SECONDS);
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("token", token);
+        map.put("doctor", doctor);
+        if (doctor.getDeptId() != null) {
+            FsDepartment department = departmentService.selectFsDepartmentByDeptId(doctor.getDeptId());
+            map.put("deptName", department != null ? department.getDeptName() : "");
+        }
+        map.put("roles", fsDoctorPermissionService.getRolePermission(doctor.getDoctorId()));
+        map.put("permissions", fsDoctorPermissionService.getMenuPermission(doctor.getDoctorId()));
+        if (includeRouters) {
+            map.put("routers", fsDoctorPermissionService.getRouters(doctor.getDoctorId()));
+        }
+        return R.ok(map);
     }
 
     public String validateCaptcha(String username, String code, String uuid) {
@@ -281,6 +280,22 @@ public class DoctorController extends  AppBaseController {
         return R.ok("认证成功");
     }
 
+    @Login
+    @ApiOperation("获取医生会话信息(刷新页面后恢复登录态)")
+    @GetMapping("/getInfo")
+    public R getInfo(HttpServletRequest request){
+        Long doctorId = Long.parseLong(getDoctorId());
+        FsDoctor doctor = doctorService.selectFsDoctorByDoctorId(doctorId);
+        if (doctor == null) {
+            return R.error("用户不存在");
+        }
+        Map<String,Object> map = new HashMap<>();
+        map.put("doctor", doctor);
+        map.put("roles", fsDoctorPermissionService.getRolePermission(doctorId));
+        map.put("permissions", fsDoctorPermissionService.getMenuPermission(doctorId));
+        return R.ok(map);
+    }
+
 
     @Login
     @ApiOperation("修改信息")
@@ -467,5 +482,30 @@ public class DoctorController extends  AppBaseController {
         return smsService.sendTSms(doctor.getMobile(),code+"");
     }
 
+    /**
+     * 根据租户编码切换租户数据源(未携带则保持主库)。
+     *
+     * @param tenantCode 租户编码
+     * @return 错误信息;null 表示成功
+     */
+    private String checkAndSwitchTenant(String tenantCode) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (StringUtils.isBlank(tenantCode)) {
+            return null;
+        }
+        TenantInfo tenantInfo = tenantInfoService.selectTenantInfoByCode(tenantCode);
+        if (tenantInfo == null) {
+            return "租户不存在";
+        }
+        if (!Integer.valueOf(1).equals(tenantInfo.getStatus())) {
+            return "租户已禁用";
+        }
+        if (tenantInfo.getExpireTime() != null && tenantInfo.getExpireTime().before(new Date())) {
+            return "租户已过期";
+        }
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        return null;
+    }
+
 
 }

+ 28 - 0
java/fs-doctor-app/src/main/java/com/fs/app/controller/PrescribeController.java

@@ -331,6 +331,34 @@ public class PrescribeController extends  AppBaseController {
         String url=prescribeService.getPrescribeCodeUrl(prescribeId);
         return R.ok().put("url",url);
     }
+
+    @Login
+    @ApiOperation("直接保存处方")
+    @Transactional
+    @PostMapping("/savePrescribe")
+    public R savePrescribe(@RequestBody FsPrescribeSaveParam param) {
+        if (param.getDrugs() == null || param.getDrugs().isEmpty()) {
+            return R.error("请选择药品");
+        }
+        if (StringUtils.isEmpty(param.getDiagnose())) {
+            return R.error("请填写诊断/医嘱");
+        }
+        FsPrescribe prescribe = new FsPrescribe();
+        prescribe.setDoctorId(Long.parseLong(getDoctorId()));
+        prescribe.setDiagnose(param.getDiagnose());
+        prescribe.setPatientName(param.getPatientName());
+        prescribe.setPrescribeType(param.getPrescribeType());
+        prescribe.setStatus(0); // 待审核
+        prescribe.setCreateTime(new Date());
+        if (prescribeService.insertFsPrescribe(prescribe) > 0) {
+            for (FsPrescribeDrug drug : param.getDrugs()) {
+                drug.setPrescribeId(prescribe.getPrescribeId());
+                prescribeDrugService.insertFsPrescribeDrug(drug);
+            }
+            return R.ok("保存成功").put("prescribeId", prescribe.getPrescribeId());
+        }
+        return R.error("保存失败");
+    }
     @PostMapping("/test")
     public R test(@RequestBody HashMap<String,String> map) throws JsonProcessingException {
         ObjectMapper objectMapper = new ObjectMapper();

+ 4 - 0
java/fs-doctor-app/src/main/java/com/fs/app/param/DoctorLoginParam.java

@@ -23,4 +23,8 @@ public class DoctorLoginParam implements Serializable {
      */
     private String uuid = "";
     private String jpushId;
+    /**
+     * 租户编码(多租户路由用,登录时区分租户库)
+     */
+    private String tenantCode;
 }

+ 149 - 0
java/fs-doctor-app/src/main/resources/application-dev.yml

@@ -0,0 +1,149 @@
+# 数据源配置
+spring:
+    # redis 配置
+    redis:
+        # 地址
+        host: localhost
+#        host: 172.27.0.7
+        # 端口,默认为6379
+        port: 6379
+        # 数据库索引
+        database: 0
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 20s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 8
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+    datasource:
+        mysql:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                initialSize: 5
+                minIdle: 10
+                maxActive: 20
+                maxWait: 60000
+                timeBetweenEvictionRunsMillis: 60000
+                minEvictableIdleTimeMillis: 300000
+                maxEvictableIdleTimeMillis: 900000
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://cq-cdb-68dfoj21.sql.tencentcdb.com:23570/ylrz_saas?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
+                    username: root
+                    password: Ylrztek260703!3@.
+                    # 初始连接数
+                    initialSize: 5
+                    # 最小连接池数量
+                    minIdle: 10
+                    # 最大连接池数量
+                    maxActive: 20
+                    # 配置获取连接等待超时的时间
+                    maxWait: 60000
+                    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                    timeBetweenEvictionRunsMillis: 60000
+                    # 配置一个连接在池中最小生存的时间,单位是毫秒
+                    minEvictableIdleTimeMillis: 300000
+                    # 配置一个连接在池中最大生存的时间,单位是毫秒
+                    maxEvictableIdleTimeMillis: 900000
+                    # 配置检测连接是否有效
+                    validationQuery: SELECT 1 FROM DUAL
+                    testWhileIdle: true
+                    testOnBorrow: false
+                    testOnReturn: false
+                    webStatFilter:
+                        enabled: true
+                    statViewServlet:
+                        enabled: true
+                        # 设置白名单,不填则允许所有访问
+                        allow:
+                        url-pattern: /druid/*
+                        # 控制台管理用户名和密码
+                        login-username: fs
+                        login-password: 123456
+                    filter:
+                        stat:
+                            enabled: true
+                            # 慢SQL记录
+                            log-slow-sql: true
+                            slow-sql-millis: 1000
+                            merge-sql: true
+                        wall:
+                            config:
+                                multi-statement-allow: true
+        easycall:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: easycallYl@123..
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
+
+# 启动时跳过微信支付和商户配置初始化(fs-admin 模块不需要)
+fs:
+  startup:
+    init-wechat-config: false
+    init-merchant-config: false
+  tenant:
+    upgrade:
+      classpath-fallback: true
+  # JWT 配置(医生端登录 token 签名)
+  jwt:
+    # 加密秘钥
+    secret: f4e2e52034348f86b67cde581c0f9eb5
+    # token有效时长,7天,单位秒
+    expire: 31536000
+    header: AppToken

+ 5 - 2
java/fs-doctor-app/src/main/resources/application.yml

@@ -14,5 +14,8 @@ xss:
 # Spring配置
 spring:
   profiles:
-#    active: dev
-    active: dev-yjb
+    active: dev
+    # 引入共享配置(mybatis-plus / mybatis / pagehelper / token / fs.version 等来自 fs-service 的 application-common.yml,
+    # qw.provider.* / baidu / logging 等来自 fs-service 的 application-config-dev.yml)
+    include: common,config-dev
+#    active: dev-yjb

+ 39 - 0
java/fs-service/src/main/java/com/fs/tenant/mapper/TenantInfoMapper.java

@@ -6,6 +6,7 @@ import com.fs.billing.domain.FeePlanItem;
 import com.fs.common.core.domain.entity.SysMenu;
 
 import com.fs.common.core.domain.entity.TenantCompanyMenu;
+import com.fs.common.core.domain.entity.TenantDoctorMenu;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.qw.domain.QwRestrictionPushRecord;
 import com.fs.tenant.domain.TenantInfo;
@@ -159,6 +160,44 @@ public interface TenantInfoMapper extends BaseMapper<TenantInfo> {
     /** 检查租户库 company_menu 中是否存在指定菜单(须在租户数据源下调用) */
     int countTenantComMenuById(@Param("menuId") Long menuId);
 
+    // ========== 医生端菜单(模板 tenant_doctor_menu / 租户库 fs_doctor_menu) ==========
+
+    TenantDoctorMenu selectDoctorMenuById(Long menuId);
+
+    List<TenantDoctorMenu> selectDoctorMenuList(TenantDoctorMenu menu);
+
+    TenantDoctorMenu checkDoctorMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId);
+
+    int insertDoctorMenu(TenantDoctorMenu menu);
+
+    int updateDoctorMenu(TenantDoctorMenu menu);
+
+    int hasChildByDoctorMenuId(Long menuId);
+
+    int deleteDoctorMenuById(Long menuId);
+
+    /** 租户库 fs_doctor_menu 列表(须在租户数据源下调用) */
+    List<TenantDoctorMenu> selectTenantDoctorMenu();
+
+    int updatePitchDoctorMenu(@Param("selected") List<Long> selected);
+
+    int updateUnPitchDoctorMenu(@Param("unSelected") List<Long> unSelected);
+
+    /** 租户库 fs_doctor_menu 已有 menu_id(须在租户数据源下调用) */
+    List<Long> selectTenantDbDoctorMenuIds();
+
+    List<TenantDoctorMenu> getTenDoctorMenuByIds(List<Long> needAddIds);
+
+    int addDoctorMenu(List<TenantDoctorMenu> addDoctorMenu);
+
+    int upsertDoctorMenu(@Param("list") List<TenantDoctorMenu> list);
+
+    int deleteTenantDoctorMenuByIds(@Param("menuIds") List<Long> menuIds);
+
+    int deleteDoctorRoleMenuByMenuIds(@Param("menuIds") List<Long> menuIds);
+
+    int countTenantDoctorMenuById(@Param("menuId") Long menuId);
+
     TenantInfo getTenByCode(String code);
 
     List<FeePlanItem> selectFeeItem(String feePlanCode);

+ 25 - 0
java/fs-service/src/main/java/com/fs/tenant/service/TenantInfoService.java

@@ -7,6 +7,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysMenu;
 ;
 import com.fs.common.core.domain.entity.TenantCompanyMenu;
+import com.fs.common.core.domain.entity.TenantDoctorMenu;
 import com.fs.tenant.domain.TenantInfo;
 import com.fs.tenant.vo.TenantInfoShowVo;
 
@@ -118,6 +119,30 @@ public interface TenantInfoService extends IService<TenantInfo> {
 
     int deleteComMenuById(Long menuId);
 
+    // ========== 医生端菜单(模板 tenant_doctor_menu / 租户库 fs_doctor_menu) ==========
+
+    R menuChangeDoctor(List<TenantDoctorMenu> doctorMenus);
+
+    List<Long> expandDoctorMenuIdsWithAncestors(List<Long> menuIds, List<TenantDoctorMenu> allMenus);
+
+    R menuEditDoctor(List<Long> selected, List<Long> unSelected, List<TenantDoctorMenu> addDoctorMenu);
+
+    List<TenantDoctorMenu> selectDoctorMenuList(TenantDoctorMenu menu, Long userId);
+
+    TenantDoctorMenu getTenantDoctorMenu(Long menuId);
+
+    String checkDoctorMenuNameUnique(TenantDoctorMenu menu);
+
+    int insertDoctorMenu(TenantDoctorMenu menu);
+
+    int updateDoctorMenu(TenantDoctorMenu menu);
+
+    boolean hasChildByDoctorMenuId(Long menuId);
+
+    int deleteDoctorMenuById(Long menuId);
+
+    R menuAssignReplaceDoctor(List<Long> selected, List<TenantDoctorMenu> assignDoctorMenu);
+
     List<FeePlanItem> selectFeeItem(String feePlanCode);
 
     Map<String, Object>  getYesterDayTraffic(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item,TenantInfo tenant);

+ 137 - 0
java/fs-service/src/main/java/com/fs/tenant/service/impl/TenantInfoServiceImpl.java

@@ -16,6 +16,7 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.common.core.domain.TreeSelect;
 import com.fs.common.core.domain.entity.SysMenu;
 import com.fs.common.core.domain.entity.TenantCompanyMenu;
+import com.fs.common.core.domain.entity.TenantDoctorMenu;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
@@ -612,6 +613,142 @@ public class TenantInfoServiceImpl extends ServiceImpl<TenantInfoMapper, TenantI
         return baseMapper.deleteComMenuById(menuId);
     }
 
+    // ========== 医生端菜单(模板 tenant_doctor_menu / 租户库 fs_doctor_menu) ==========
+
+    @Override
+    public R menuChangeDoctor(List<TenantDoctorMenu> doctorMenus) {
+        List<TenantDoctorMenu> menuList = baseMapper.selectTenantDoctorMenu();
+        List<TenantDoctorMenu> result = mergeDoctorMenu(doctorMenus, menuList);
+        List<Long> tenantMenuIds = menuList.stream()
+                .map(TenantDoctorMenu::getMenuId)
+                .collect(Collectors.toList());
+        List<Long> checkedKeys = expandDoctorMenuIdsWithAncestors(tenantMenuIds, doctorMenus);
+        return R.ok().put("menus", result).put("checkedKeys", checkedKeys);
+    }
+
+    @Override
+    public List<Long> expandDoctorMenuIdsWithAncestors(List<Long> menuIds, List<TenantDoctorMenu> allMenus) {
+        if (CollectionUtils.isEmpty(menuIds) || CollectionUtils.isEmpty(allMenus)) {
+            return new ArrayList<>(emptyIfNull(menuIds));
+        }
+        Map<Long, Long> parentById = allMenus.stream()
+                .collect(Collectors.toMap(TenantDoctorMenu::getMenuId, TenantDoctorMenu::getParentId, (a, b) -> a));
+        return expandMenuIdsWithAncestors(menuIds, parentById);
+    }
+
+    @Override
+    public R menuEditDoctor(List<Long> selected, List<Long> unSelected, List<TenantDoctorMenu> addDoctorMenu) {
+        if (!CollectionUtils.isEmpty(selected)) {
+            baseMapper.updatePitchDoctorMenu(selected);
+        }
+        if (!CollectionUtils.isEmpty(unSelected)) {
+            baseMapper.updateUnPitchDoctorMenu(unSelected);
+        }
+        if (!CollectionUtils.isEmpty(addDoctorMenu)) {
+            baseMapper.addDoctorMenu(addDoctorMenu);
+        }
+        return R.ok();
+    }
+
+    private List<TenantDoctorMenu> mergeDoctorMenu(List<TenantDoctorMenu> standardList, List<TenantDoctorMenu> tenantList) {
+        Map<Long, TenantDoctorMenu> tenantMap = tenantList.stream()
+                .collect(Collectors.toMap(TenantDoctorMenu::getMenuId, item -> item, (a, b) -> a));
+        List<TenantDoctorMenu> result = new ArrayList<>(standardList.size());
+        for (TenantDoctorMenu standard : standardList) {
+            TenantDoctorMenu tenant = tenantMap.get(standard.getMenuId());
+            if (tenant != null) {
+                tenant.setVisible("0");
+                result.add(tenant);
+            } else {
+                TenantDoctorMenu unassigned = new TenantDoctorMenu();
+                BeanUtil.copyProperties(standard, unassigned);
+                unassigned.setVisible("1");
+                result.add(unassigned);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public List<TenantDoctorMenu> selectDoctorMenuList(TenantDoctorMenu menu, Long userId) {
+        return baseMapper.selectDoctorMenuList(menu);
+    }
+
+    @Override
+    public TenantDoctorMenu getTenantDoctorMenu(Long menuId) {
+        return baseMapper.selectDoctorMenuById(menuId);
+    }
+
+    @Override
+    public String checkDoctorMenuNameUnique(TenantDoctorMenu menu) {
+        Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId();
+        TenantDoctorMenu info = baseMapper.checkDoctorMenuNameUnique(menu.getMenuName(), menu.getParentId());
+        if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    @Override
+    public int insertDoctorMenu(TenantDoctorMenu menu) {
+        return baseMapper.insertDoctorMenu(menu);
+    }
+
+    @Override
+    public int updateDoctorMenu(TenantDoctorMenu menu) {
+        return baseMapper.updateDoctorMenu(menu);
+    }
+
+    @Override
+    public boolean hasChildByDoctorMenuId(Long menuId) {
+        int result = baseMapper.hasChildByDoctorMenuId(menuId);
+        return result > 0;
+    }
+
+    @Override
+    public int deleteDoctorMenuById(Long menuId) {
+        return baseMapper.deleteDoctorMenuById(menuId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R menuAssignReplaceDoctor(List<Long> selected, List<TenantDoctorMenu> assignDoctorMenu) {
+        Set<Long> selectedSet = new HashSet<>(emptyIfNull(selected));
+        syncDoctorMenuAssign(selectedSet, emptyListIfNull(assignDoctorMenu));
+        return R.ok();
+    }
+
+    private void syncDoctorMenuAssign(Set<Long> selectedSet, List<TenantDoctorMenu> assignDoctorMenu) {
+        Set<Long> existSet = new HashSet<>(emptyIfNull(baseMapper.selectTenantDbDoctorMenuIds()));
+        List<Long> toRemove = existSet.stream()
+                .filter(id -> !selectedSet.contains(id))
+                .collect(Collectors.toList());
+        removeTenantDoctorMenus(toRemove);
+        batchUpsertDoctorMenu(assignDoctorMenu);
+    }
+
+    private void batchUpsertDoctorMenu(List<TenantDoctorMenu> menus) {
+        if (CollectionUtils.isEmpty(menus)) {
+            return;
+        }
+        for (int i = 0; i < menus.size(); i += MENU_UPSERT_BATCH_SIZE) {
+            int end = Math.min(i + MENU_UPSERT_BATCH_SIZE, menus.size());
+            baseMapper.upsertDoctorMenu(new ArrayList<>(menus.subList(i, end)));
+        }
+    }
+
+    private void removeTenantDoctorMenus(List<Long> menuIds) {
+        if (CollectionUtils.isEmpty(menuIds)) {
+            return;
+        }
+        for (int i = 0; i < menuIds.size(); i += MENU_UPSERT_BATCH_SIZE) {
+            int end = Math.min(i + MENU_UPSERT_BATCH_SIZE, menuIds.size());
+            List<Long> batch = menuIds.subList(i, end);
+            baseMapper.deleteDoctorRoleMenuByMenuIds(batch);
+            baseMapper.deleteTenantDoctorMenuByIds(batch);
+        }
+    }
+
     @Override
     public List<FeePlanItem> selectFeeItem(String feePlanCode) {
         return baseMapper.selectFeeItem(feePlanCode);

+ 17 - 0
java/fs-service/src/main/resources/db/tenant-initData.sql

@@ -6008,3 +6008,20 @@ INSERT IGNORE INTO company_role_menu (role_id, menu_id) SELECT role_id, 329822 F
 INSERT IGNORE INTO company_role_menu (role_id, menu_id) SELECT role_id, 329823 FROM company_role_menu WHERE menu_id = 32981;
 INSERT IGNORE INTO company_role_menu (role_id, menu_id) SELECT role_id, 329824 FROM company_role_menu WHERE menu_id = 32981;
 
+-- 医生端默认菜单树种子(IM问诊/患者管理/电子处方/用药咨询/处方审核)
+INSERT INTO `tenant_doctor_menu`
+(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
+VALUES
+    (1, 'IM问诊', 0, 1, 'consult', 'doctor/consultWorkbench/index', 1, 0, 'C', '0', '0', 'doctor:consult:list', 'form', 'admin', NOW(), 'IM问诊'),
+    (2, '患者管理', 0, 2, 'patient', 'doctor/patientRecord/index', 1, 0, 'C', '0', '0', 'doctor:patient:list', 'user', 'admin', NOW(), '患者管理'),
+    (3, '电子处方', 0, 3, 'prescribe', 'doctor/prescribeCreate/index', 1, 0, 'C', '0', '0', 'doctor:prescribe:list', 'edit', 'admin', NOW(), '电子处方'),
+    (4, '用药咨询', 0, 2, 'medicineAdvice', 'doctor/medicineAdvice/index', 1, 0, 'C', '0', '0', 'doctor:medicineAdvice:list', 'user', 'admin', NOW(), '用药咨询'),
+    (5, '处方审核', 0, 3, 'prescriptionAudit', 'doctor/prescriptionAudit/index', 1, 0, 'C', '0', '0', 'doctor:prescriptionAudit:list', 'edit', 'admin', NOW(), '处方审核')
+ON DUPLICATE KEY UPDATE
+                     `menu_name` = VALUES(`menu_name`),
+                     `parent_id` = VALUES(`parent_id`),
+                     `order_num` = VALUES(`order_num`),
+                     `path` = VALUES(`path`),
+                     `component` = VALUES(`component`),
+                     `perms` = VALUES(`perms`),
+                     `update_time` = NOW();

+ 197 - 0
java/fs-service/src/main/resources/db/tenant-initTable.sql

@@ -21700,6 +21700,203 @@ CREATE TABLE `proxy_purchase_split` (
                                         INDEX `idx_month` (`month`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代理进货分成记录表';
 
+-- ----------------------------
+-- Table structure for fs_doctor_role
+-- ----------------------------
+DROP TABLE IF EXISTS `fs_doctor_role`;
+CREATE TABLE `fs_doctor_role` (
+    `role_id`             bigint       NOT NULL AUTO_INCREMENT COMMENT '角色ID',
+    `role_name`           varchar(30)  NOT NULL COMMENT '角色名称',
+    `role_key`            varchar(100) NOT NULL COMMENT '角色权限字符串',
+    `role_sort`           int          NOT NULL COMMENT '显示顺序',
+    `data_scope`          char(1)      DEFAULT '1' COMMENT '数据范围(1全部 2自定义 3本部门 4本部门及以下)',
+    `menu_check_strictly` tinyint(1)   DEFAULT 1 COMMENT '菜单树选择项是否关联显示',
+    `status`              char(1)      NOT NULL COMMENT '角色状态(0正常 1停用)',
+    `del_flag`            char(1)      DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
+    `create_by`           varchar(64)  DEFAULT '' COMMENT '创建者',
+    `create_time`         datetime     DEFAULT NULL COMMENT '创建时间',
+    `update_by`           varchar(64)  DEFAULT '' COMMENT '更新者',
+    `update_time`         datetime     DEFAULT NULL COMMENT '更新时间',
+    `remark`              varchar(500) DEFAULT NULL COMMENT '备注',
+    PRIMARY KEY (`role_id`)
+) ENGINE=InnoDB COMMENT='医生角色表' ROW_FORMAT=DYNAMIC;
+
+-- ----------------------------
+-- Table structure for fs_doctor_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `fs_doctor_menu`;
+CREATE TABLE `fs_doctor_menu` (
+    `menu_id`     bigint       NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
+    `menu_name`   varchar(50)  NOT NULL COMMENT '菜单名称',
+    `parent_id`   bigint       DEFAULT 0 COMMENT '父菜单ID',
+    `order_num`   int          DEFAULT 0 COMMENT '显示顺序',
+    `path`        varchar(200) DEFAULT '' COMMENT '路由地址',
+    `component`   varchar(255) DEFAULT NULL COMMENT '组件路径',
+    `is_frame`    int          DEFAULT 1 COMMENT '是否外链(0是 1否)',
+    `is_cache`    int          DEFAULT 0 COMMENT '是否缓存(0缓存 1不缓存)',
+    `menu_type`   char(1)      DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
+    `visible`     char(1)      DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
+    `status`      char(1)      DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
+    `perms`       varchar(100) DEFAULT NULL COMMENT '权限标识',
+    `icon`        varchar(100) DEFAULT '#' COMMENT '菜单图标',
+    `create_by`   varchar(64)  DEFAULT '' COMMENT '创建者',
+    `create_time` datetime     DEFAULT NULL COMMENT '创建时间',
+    `update_by`   varchar(64)  DEFAULT '' COMMENT '更新者',
+    `update_time` datetime     DEFAULT NULL COMMENT '更新时间',
+    `remark`      varchar(500) DEFAULT '' COMMENT '备注',
+    PRIMARY KEY (`menu_id`)
+) ENGINE=InnoDB COMMENT='医生菜单权限表' ROW_FORMAT=DYNAMIC;
+
+-- ----------------------------
+-- Table structure for fs_doctor_role_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `fs_doctor_role_menu`;
+CREATE TABLE `fs_doctor_role_menu` (
+    `role_id` bigint NOT NULL COMMENT '角色ID',
+    `menu_id` bigint NOT NULL COMMENT '菜单ID',
+    PRIMARY KEY (`role_id`, `menu_id`)
+) ENGINE=InnoDB COMMENT='医生角色和菜单关联表' ROW_FORMAT=DYNAMIC;
+
+-- ----------------------------
+-- Table structure for fs_doctor_user_role
+-- ----------------------------
+DROP TABLE IF EXISTS `fs_doctor_user_role`;
+CREATE TABLE `fs_doctor_user_role` (
+    `doctor_id` bigint NOT NULL COMMENT '医生ID(fs_doctor.doctor_id)',
+    `role_id`   bigint NOT NULL COMMENT '角色ID',
+    PRIMARY KEY (`doctor_id`, `role_id`)
+) ENGINE=InnoDB COMMENT='医生用户和角色关联表' ROW_FORMAT=DYNAMIC;
+
+-- 医生端 RBAC 四表(幂等建表,供存量租户库补齐)
+CREATE TABLE IF NOT EXISTS `fs_doctor_role` (
+                                                `role_id`             bigint       NOT NULL AUTO_INCREMENT COMMENT '角色ID',
+                                                `role_name`           varchar(30)  NOT NULL COMMENT '角色名称',
+                                                `role_key`            varchar(100) NOT NULL COMMENT '角色权限字符串',
+                                                `role_sort`           int          NOT NULL COMMENT '显示顺序',
+                                                `data_scope`          char(1)      DEFAULT '1' COMMENT '数据范围(1全部 2自定义 3本部门 4本部门及以下)',
+                                                `menu_check_strictly` tinyint(1)   DEFAULT 1 COMMENT '菜单树选择项是否关联显示',
+                                                `status`              char(1)      NOT NULL COMMENT '角色状态(0正常 1停用)',
+                                                `del_flag`            char(1)      DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
+                                                `create_by`           varchar(64)  DEFAULT '' COMMENT '创建者',
+                                                `create_time`         datetime     DEFAULT NULL COMMENT '创建时间',
+                                                `update_by`           varchar(64)  DEFAULT '' COMMENT '更新者',
+                                                `update_time`         datetime     DEFAULT NULL COMMENT '更新时间',
+                                                `remark`              varchar(500) DEFAULT NULL COMMENT '备注',
+                                                PRIMARY KEY (`role_id`)
+) ENGINE=InnoDB COMMENT='医生角色表' ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `fs_doctor_menu` (
+                                                `menu_id`     bigint       NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
+                                                `menu_name`   varchar(50)  NOT NULL COMMENT '菜单名称',
+                                                `parent_id`   bigint       DEFAULT 0 COMMENT '父菜单ID',
+                                                `order_num`   int          DEFAULT 0 COMMENT '显示顺序',
+                                                `path`        varchar(200) DEFAULT '' COMMENT '路由地址',
+                                                `component`   varchar(255) DEFAULT NULL COMMENT '组件路径',
+                                                `is_frame`    int          DEFAULT 1 COMMENT '是否外链(0是 1否)',
+                                                `is_cache`    int          DEFAULT 0 COMMENT '是否缓存(0缓存 1不缓存)',
+                                                `menu_type`   char(1)      DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
+                                                `visible`     char(1)      DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
+                                                `status`      char(1)      DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
+                                                `perms`       varchar(100) DEFAULT NULL COMMENT '权限标识',
+                                                `icon`        varchar(100) DEFAULT '#' COMMENT '菜单图标',
+                                                `create_by`   varchar(64)  DEFAULT '' COMMENT '创建者',
+                                                `create_time` datetime     DEFAULT NULL COMMENT '创建时间',
+                                                `update_by`   varchar(64)  DEFAULT '' COMMENT '更新者',
+                                                `update_time` datetime     DEFAULT NULL COMMENT '更新时间',
+                                                `remark`      varchar(500) DEFAULT '' COMMENT '备注',
+                                                PRIMARY KEY (`menu_id`)
+) ENGINE=InnoDB COMMENT='医生菜单权限表' ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `fs_doctor_role_menu` (
+                                                     `role_id` bigint NOT NULL COMMENT '角色ID',
+                                                     `menu_id` bigint NOT NULL COMMENT '菜单ID',
+                                                     PRIMARY KEY (`role_id`, `menu_id`)
+) ENGINE=InnoDB COMMENT='医生角色和菜单关联表' ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `fs_doctor_user_role` (
+                                                     `doctor_id` bigint NOT NULL COMMENT '医生ID(fs_doctor.doctor_id)',
+                                                     `role_id`   bigint NOT NULL COMMENT '角色ID',
+                                                     PRIMARY KEY (`doctor_id`, `role_id`)
+) ENGINE=InnoDB COMMENT='医生用户和角色关联表' ROW_FORMAT=DYNAMIC;
+
+-- 医生端标准菜单模板表(主库)
+CREATE TABLE IF NOT EXISTS `tenant_doctor_menu` (
+                                                    `menu_id`     bigint       NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
+                                                    `menu_name`   varchar(50)  NOT NULL COMMENT '菜单名称',
+                                                    `parent_id`   bigint       DEFAULT 0 COMMENT '父菜单ID',
+                                                    `order_num`   int          DEFAULT 0 COMMENT '显示顺序',
+                                                    `path`        varchar(200) DEFAULT '' COMMENT '路由地址',
+                                                    `component`   varchar(255) DEFAULT NULL COMMENT '组件路径',
+                                                    `is_frame`    int          DEFAULT 1 COMMENT '是否外链(0是 1否)',
+                                                    `is_cache`    int          DEFAULT 0 COMMENT '是否缓存(0缓存 1不缓存)',
+                                                    `menu_type`   char(1)      DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
+                                                    `visible`     char(1)      DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
+                                                    `status`      char(1)      DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
+                                                    `perms`       varchar(100) DEFAULT NULL COMMENT '权限标识',
+                                                    `icon`        varchar(100) DEFAULT '#' COMMENT '菜单图标',
+                                                    `create_by`   varchar(64)  DEFAULT '' COMMENT '创建者',
+                                                    `create_time` datetime     DEFAULT NULL COMMENT '创建时间',
+                                                    `update_by`   varchar(64)  DEFAULT '' COMMENT '更新者',
+                                                    `update_time` datetime     DEFAULT NULL COMMENT '更新时间',
+                                                    `remark`      varchar(500) DEFAULT '' COMMENT '备注',
+                                                    PRIMARY KEY (`menu_id`)
+) ENGINE=InnoDB COMMENT='医生端标准菜单模板表' ROW_FORMAT=DYNAMIC;
+
+
+-- 医生端 RBAC 四表(幂等建表,供存量租户库补齐)
+CREATE TABLE IF NOT EXISTS `fs_doctor_role` (
+                                                `role_id`             bigint       NOT NULL AUTO_INCREMENT COMMENT '角色ID',
+                                                `role_name`           varchar(30)  NOT NULL COMMENT '角色名称',
+                                                `role_key`            varchar(100) NOT NULL COMMENT '角色权限字符串',
+                                                `role_sort`           int          NOT NULL COMMENT '显示顺序',
+                                                `data_scope`          char(1)      DEFAULT '1' COMMENT '数据范围(1全部 2自定义 3本部门 4本部门及以下)',
+                                                `menu_check_strictly` tinyint(1)   DEFAULT 1 COMMENT '菜单树选择项是否关联显示',
+                                                `status`              char(1)      NOT NULL COMMENT '角色状态(0正常 1停用)',
+                                                `del_flag`            char(1)      DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
+                                                `create_by`           varchar(64)  DEFAULT '' COMMENT '创建者',
+                                                `create_time`         datetime     DEFAULT NULL COMMENT '创建时间',
+                                                `update_by`           varchar(64)  DEFAULT '' COMMENT '更新者',
+                                                `update_time`         datetime     DEFAULT NULL COMMENT '更新时间',
+                                                `remark`              varchar(500) DEFAULT NULL COMMENT '备注',
+                                                PRIMARY KEY (`role_id`)
+) ENGINE=InnoDB COMMENT='医生角色表' ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `fs_doctor_menu` (
+                                                `menu_id`     bigint       NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
+                                                `menu_name`   varchar(50)  NOT NULL COMMENT '菜单名称',
+                                                `parent_id`   bigint       DEFAULT 0 COMMENT '父菜单ID',
+                                                `order_num`   int          DEFAULT 0 COMMENT '显示顺序',
+                                                `path`        varchar(200) DEFAULT '' COMMENT '路由地址',
+                                                `component`   varchar(255) DEFAULT NULL COMMENT '组件路径',
+                                                `is_frame`    int          DEFAULT 1 COMMENT '是否外链(0是 1否)',
+                                                `is_cache`    int          DEFAULT 0 COMMENT '是否缓存(0缓存 1不缓存)',
+                                                `menu_type`   char(1)      DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
+                                                `visible`     char(1)      DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
+                                                `status`      char(1)      DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
+                                                `perms`       varchar(100) DEFAULT NULL COMMENT '权限标识',
+                                                `icon`        varchar(100) DEFAULT '#' COMMENT '菜单图标',
+                                                `create_by`   varchar(64)  DEFAULT '' COMMENT '创建者',
+                                                `create_time` datetime     DEFAULT NULL COMMENT '创建时间',
+                                                `update_by`   varchar(64)  DEFAULT '' COMMENT '更新者',
+                                                `update_time` datetime     DEFAULT NULL COMMENT '更新时间',
+                                                `remark`      varchar(500) DEFAULT '' COMMENT '备注',
+                                                PRIMARY KEY (`menu_id`)
+) ENGINE=InnoDB COMMENT='医生菜单权限表' ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `fs_doctor_role_menu` (
+                                                     `role_id` bigint NOT NULL COMMENT '角色ID',
+                                                     `menu_id` bigint NOT NULL COMMENT '菜单ID',
+                                                     PRIMARY KEY (`role_id`, `menu_id`)
+) ENGINE=InnoDB COMMENT='医生角色和菜单关联表' ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `fs_doctor_user_role` (
+                                                     `doctor_id` bigint NOT NULL COMMENT '医生ID(fs_doctor.doctor_id)',
+                                                     `role_id`   bigint NOT NULL COMMENT '角色ID',
+                                                     PRIMARY KEY (`doctor_id`, `role_id`)
+) ENGINE=InnoDB COMMENT='医生用户和角色关联表' ROW_FORMAT=DYNAMIC;
+
+
+
 -- ============================================================
 -- [MERGE] structure DDL from ddl_merge_from_ylrz_saas.sql (create/alter/index)
 -- ============================================================

+ 239 - 0
java/fs-service/src/main/resources/mapper/tenant/TenantInfoMapper.xml

@@ -789,4 +789,243 @@
     <select id="countTenantComMenuById" resultType="int">
         SELECT COUNT(1) FROM `company_menu` WHERE menu_id = #{menuId}
     </select>
+
+    <!-- ==================== 医生端菜单(模板 tenant_doctor_menu / 租户库 fs_doctor_menu) ==================== -->
+
+    <resultMap type="com.fs.common.core.domain.entity.TenantDoctorMenu" id="DoctorMenuResult">
+        <result property="menuId"     column="menu_id"     />
+        <result property="menuName"   column="menu_name"   />
+        <result property="parentId"   column="parent_id"   />
+        <result property="orderNum"   column="order_num"   />
+        <result property="path"       column="path"        />
+        <result property="component"  column="component"   />
+        <result property="isFrame"    column="is_frame"    />
+        <result property="isCache"    column="is_cache"    />
+        <result property="menuType"   column="menu_type"   />
+        <result property="visible"    column="visible"     />
+        <result property="status"     column="status"      />
+        <result property="perms"      column="perms"       />
+        <result property="icon"       column="icon"        />
+        <result property="createBy"   column="create_by"   />
+        <result property="createTime" column="create_time" />
+        <result property="updateBy"   column="update_by"   />
+        <result property="updateTime" column="update_time" />
+        <result property="remark"     column="remark"      />
+    </resultMap>
+
+    <sql id="selectDoctorMenuVo">
+        select menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark from tenant_doctor_menu
+    </sql>
+
+    <select id="selectDoctorMenuById" resultMap="DoctorMenuResult">
+        <include refid="selectDoctorMenuVo"/>
+        where menu_id = #{menuId}
+    </select>
+
+    <select id="selectDoctorMenuList" resultMap="DoctorMenuResult">
+        <include refid="selectDoctorMenuVo"/>
+        <where>
+            <if test="menuName != null and menuName != ''"> and menu_name like concat('%', #{menuName}, '%')</if>
+            <if test="visible != null and visible != ''"> and visible = #{visible}</if>
+            <if test="status != null and status != ''"> and status = #{status}</if>
+        </where>
+        order by parent_id, order_num
+    </select>
+
+    <select id="checkDoctorMenuNameUnique" resultType="com.fs.common.core.domain.entity.TenantDoctorMenu">
+        <include refid="selectDoctorMenuVo"/>
+        where menu_name=#{menuName} and parent_id = #{parentId} limit 1
+    </select>
+
+    <select id="hasChildByDoctorMenuId" resultType="java.lang.Integer">
+        select count(1) from tenant_doctor_menu where parent_id = #{menuId}
+    </select>
+
+    <!-- 租户库 fs_doctor_menu(须在租户数据源下调用) -->
+    <select id="selectTenantDoctorMenu" resultType="com.fs.common.core.domain.entity.TenantDoctorMenu">
+        select menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_by, create_time, update_by, update_time, remark
+        from fs_doctor_menu
+    </select>
+
+    <select id="selectTenantDbDoctorMenuIds" resultType="java.lang.Long">
+        SELECT menu_id FROM `fs_doctor_menu`
+    </select>
+
+    <select id="getTenDoctorMenuByIds" resultType="com.fs.common.core.domain.entity.TenantDoctorMenu">
+        SELECT * FROM tenant_doctor_menu
+        WHERE menu_id IN
+        <foreach collection="list" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+    <insert id="insertDoctorMenu">
+        insert into tenant_doctor_menu
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="menuName != null and menuName != ''">menu_name,</if>
+            <if test="parentId != null">parent_id,</if>
+            <if test="orderNum != null">order_num,</if>
+            <if test="path != null">path,</if>
+            <if test="component != null">component,</if>
+            <if test="isFrame != null">is_frame,</if>
+            <if test="isCache != null">is_cache,</if>
+            <if test="menuType != null">menu_type,</if>
+            <if test="visible != null">visible,</if>
+            <if test="status != null">status,</if>
+            <if test="perms != null">perms,</if>
+            <if test="icon != null">icon,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="remark != null">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="menuName != null and menuName != ''">#{menuName},</if>
+            <if test="parentId != null">#{parentId},</if>
+            <if test="orderNum != null">#{orderNum},</if>
+            <if test="path != null">#{path},</if>
+            <if test="component != null">#{component},</if>
+            <if test="isFrame != null">#{isFrame},</if>
+            <if test="isCache != null">#{isCache},</if>
+            <if test="menuType != null">#{menuType},</if>
+            <if test="visible != null">#{visible},</if>
+            <if test="status != null">#{status},</if>
+            <if test="perms != null">#{perms},</if>
+            <if test="icon != null">#{icon},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <insert id="addDoctorMenu">
+        INSERT INTO `fs_doctor_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`,
+                                      `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`,
+                                      `create_time`, `update_by`, `update_time`, `remark`)
+        VALUES
+        <foreach collection="list" item="item" separator=",">
+            (
+            #{item.menuId},
+            #{item.menuName},
+            #{item.parentId},
+            #{item.orderNum},
+            #{item.path},
+            #{item.component},
+            #{item.isFrame},
+            #{item.isCache},
+            #{item.menuType},
+            '0',
+            #{item.status},
+            #{item.perms},
+            #{item.icon},
+            #{item.createBy},
+            NOW(),
+            #{item.updateBy},
+            null,
+            #{item.remark}
+            )
+        </foreach>
+    </insert>
+
+    <insert id="upsertDoctorMenu">
+        INSERT INTO `fs_doctor_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`,
+                                      `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`,
+                                      `create_time`, `update_by`, `update_time`, `remark`)
+        VALUES
+        <foreach collection="list" item="item" separator=",">
+            (
+            #{item.menuId},
+            #{item.menuName},
+            #{item.parentId},
+            #{item.orderNum},
+            #{item.path},
+            #{item.component},
+            #{item.isFrame},
+            #{item.isCache},
+            #{item.menuType},
+            '0',
+            #{item.status},
+            #{item.perms},
+            #{item.icon},
+            #{item.createBy},
+            NOW(),
+            #{item.updateBy},
+            NOW(),
+            #{item.remark}
+            )
+        </foreach>
+        ON DUPLICATE KEY UPDATE
+            menu_name = VALUES(menu_name),
+            parent_id = VALUES(parent_id),
+            order_num = VALUES(order_num),
+            path = VALUES(path),
+            component = VALUES(component),
+            is_frame = VALUES(is_frame),
+            is_cache = VALUES(is_cache),
+            menu_type = VALUES(menu_type),
+            visible = '0',
+            status = VALUES(status),
+            perms = VALUES(perms),
+            icon = VALUES(icon),
+            update_by = VALUES(update_by),
+            update_time = NOW(),
+            remark = VALUES(remark)
+    </insert>
+
+    <delete id="deleteTenantDoctorMenuByIds">
+        delete from fs_doctor_menu where menu_id in
+        <foreach collection="menuIds" item="menuId" open="(" separator="," close=")">
+            #{menuId}
+        </foreach>
+    </delete>
+
+    <delete id="deleteDoctorRoleMenuByMenuIds">
+        delete from fs_doctor_role_menu where menu_id in
+        <foreach collection="menuIds" item="menuId" open="(" separator="," close=")">
+            #{menuId}
+        </foreach>
+    </delete>
+
+    <update id="updateDoctorMenu">
+        update tenant_doctor_menu
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="menuName != null and menuName != ''">menu_name = #{menuName},</if>
+            <if test="parentId != null">parent_id = #{parentId},</if>
+            <if test="orderNum != null">order_num = #{orderNum},</if>
+            <if test="path != null">path = #{path},</if>
+            <if test="component != null">component = #{component},</if>
+            <if test="isFrame != null">is_frame = #{isFrame},</if>
+            <if test="isCache != null">is_cache = #{isCache},</if>
+            <if test="menuType != null">menu_type = #{menuType},</if>
+            <if test="visible != null">visible = #{visible},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="perms != null">perms = #{perms},</if>
+            <if test="icon != null">icon = #{icon},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where menu_id = #{menuId}
+    </update>
+
+    <update id="updatePitchDoctorMenu">
+        update fs_doctor_menu set visible = 0, status = 0 where menu_id in
+        <foreach item="menuId" collection="selected" open="(" separator="," close=")">
+            #{menuId}
+        </foreach>
+    </update>
+
+    <update id="updateUnPitchDoctorMenu">
+        update fs_doctor_menu set visible = 1, status = 0 where menu_id in
+        <foreach item="menuId" collection="unSelected" open="(" separator="," close=")">
+            #{menuId}
+        </foreach>
+    </update>
+
+    <delete id="deleteDoctorMenuById">
+        delete from tenant_doctor_menu where menu_id = #{menuId}
+    </delete>
+
+    <select id="countTenantDoctorMenuById" resultType="int">
+        SELECT COUNT(1) FROM `fs_doctor_menu` WHERE menu_id = #{menuId}
+    </select>
 </mapper>