Browse Source

代码提交

yjwang 1 day ago
parent
commit
0a3c7e22f7
61 changed files with 1875 additions and 145 deletions
  1. 26 13
      fs-admin/src/main/java/com/fs/company/controller/CompanyMenuController.java
  2. 96 14
      fs-admin/src/main/java/com/fs/company/controller/CompanyRoleController.java
  3. 25 0
      fs-admin/src/main/java/com/fs/his/controller/FsCompanyController.java
  4. 1 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductScrmController.java
  5. 14 0
      fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java
  6. 20 0
      fs-admin/src/main/java/com/fs/web/controller/system/SysUserController.java
  7. 71 1
      fs-common/src/main/java/com/fs/common/core/domain/entity/SysUser.java
  8. 45 0
      fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java
  9. 26 0
      fs-company-app/src/main/java/com/fs/app/param/FsAppBindLinkParam.java
  10. 20 0
      fs-company-app/src/main/java/com/fs/app/param/FsAppBindUuidParam.java
  11. 2 1
      fs-company-app/src/main/resources/application.yml
  12. 49 2
      fs-company/src/main/java/com/fs/company/controller/company/CompanyLoginController.java
  13. 27 4
      fs-company/src/main/java/com/fs/company/controller/company/CompanyMenuController.java
  14. 2 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserBindWxController.java
  15. 2 2
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  16. 92 8
      fs-company/src/main/java/com/fs/framework/service/CompanyPermissionService.java
  17. 2 1
      fs-company/src/main/resources/application.yml
  18. 11 0
      fs-service/src/main/java/com/fs/company/domain/CompanyRole.java
  19. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyMenuMapper.java
  20. 23 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyRoleMapper.java
  21. 13 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserRoleMapper.java
  22. 33 0
      fs-service/src/main/java/com/fs/company/param/CompanyAdminRoleParam.java
  23. 9 0
      fs-service/src/main/java/com/fs/company/service/ICompanyMenuService.java
  24. 10 0
      fs-service/src/main/java/com/fs/company/service/ICompanyRoleService.java
  25. 10 0
      fs-service/src/main/java/com/fs/company/service/ICompanyService.java
  26. 46 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyMenuServiceImpl.java
  27. 31 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyRoleServiceImpl.java
  28. 66 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  29. 2 0
      fs-service/src/main/java/com/fs/course/config/CourseConfig.java
  30. 19 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseLinkService.java
  31. 58 12
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java
  32. 1 1
      fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java
  33. 13 0
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  34. 76 8
      fs-service/src/main/java/com/fs/his/service/impl/FsUserBindWxServiceImpl.java
  35. 10 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  36. 1 1
      fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java
  37. 5 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java
  38. 15 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  39. 6 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java
  40. 378 35
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  41. 5 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListVO.java
  42. 17 0
      fs-service/src/main/resources/mapper/company/CompanyMenuMapper.xml
  43. 13 1
      fs-service/src/main/resources/mapper/company/CompanyRoleMapper.xml
  44. 3 1
      fs-service/src/main/resources/mapper/company/CompanyUserRoleMapper.xml
  45. 3 0
      fs-service/src/main/resources/mapper/his/FsUserBindWxMapper.xml
  46. 2 1
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  47. 45 4
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml
  48. 33 3
      fs-service/src/main/resources/mapper/system/SysUserMapper.xml
  49. 2 1
      fs-store/src/main/resources/application.yml
  50. 18 16
      fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java
  51. 149 10
      fs-user-app/src/main/java/com/fs/app/controller/UserController.java
  52. 106 0
      fs-user-app/src/main/java/com/fs/app/controller/WxH5MpController.java
  53. 45 0
      fs-user-app/src/main/java/com/fs/app/controller/store/UserScrmController.java
  54. 28 0
      fs-user-app/src/main/java/com/fs/app/param/FsUserBindPhoneParam.java
  55. 25 0
      fs-user-app/src/main/java/com/fs/app/param/FsUserCourseBeMemberH5Param.java
  56. 5 0
      fs-user-app/src/main/java/com/fs/app/param/FsUserLoginByMpParam.java
  57. 1 1
      fs-user-app/src/main/java/com/fs/app/utils/UniVerifyUtil.java
  58. 2 2
      fs-user-app/src/main/resources/application.yml
  59. 4 0
      sql/company_role_add_is_admin.sql
  60. 4 0
      sql/company_role_company_id_nullable.sql
  61. 4 0
      sql/fs_store_product_scrm_add_offline_remark.sql

+ 26 - 13
fs-admin/src/main/java/com/fs/company/controller/CompanyMenuController.java

@@ -3,21 +3,16 @@ package com.fs.company.controller;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
-import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyMenu;
 import com.fs.company.service.ICompanyMenuService;
-import com.fs.framework.web.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
-import com.fs.common.core.domain.entity.SysMenu;
-import com.fs.common.utils.ServletUtils;
 
 /**
  * 菜单权限Controller
@@ -32,8 +27,6 @@ public class CompanyMenuController extends BaseController
     @Autowired
     private ICompanyMenuService companyMenuService;
 
-    @Autowired
-    private TokenService tokenService;
     /**
      * 查询菜单权限列表
      */
@@ -105,17 +98,37 @@ public class CompanyMenuController extends BaseController
 
     /**
      * 获取菜单下拉树列表
-     */
-    /**
-     * 获取菜单下拉树列表
+     * 平台端直接返回全部销售菜单
      */
     @GetMapping("/treeselect")
     public AjaxResult treeselect(CompanyMenu menu)
     {
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        Long userId = loginUser.getUser().getUserId();
-        List<CompanyMenu> menus = companyMenuService.selectMenuList(menu, userId,"00");
+        List<CompanyMenu> menus = companyMenuService.selectMenuList(menu, null, "00");
         return AjaxResult.success(companyMenuService.buildMenuTreeSelect(menus));
     }
 
+    /**
+     * 加载对应角色菜单列表树(含已勾选菜单ID)
+     * 平台端直接返回全部销售菜单,不按登录用户过滤
+     */
+    @GetMapping(value = "/roleMenuTreeselect/{roleId}")
+    public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId)
+    {
+        CompanyMenu companyMenu = new CompanyMenu();
+        // 平台管理员:按销售端管理员身份拉取全部 company_menu
+        List<CompanyMenu> menus = companyMenuService.selectMenuList(companyMenu, null, "00");
+        AjaxResult ajax = AjaxResult.success();
+        // 新增场景 roleId 可能为0/无效,避免调用公共方法影响销售端原有逻辑
+        if (roleId != null && roleId > 0)
+        {
+            ajax.put("checkedKeys", companyMenuService.selectMenuListByRoleId(roleId));
+        }
+        else
+        {
+            ajax.put("checkedKeys", new java.util.ArrayList<Integer>());
+        }
+        ajax.put("menus", companyMenuService.buildMenuTreeSelect(menus));
+        return ajax;
+    }
+
 }

+ 96 - 14
fs-admin/src/main/java/com/fs/company/controller/CompanyRoleController.java

@@ -1,21 +1,28 @@
 package com.fs.company.controller;
 
 import com.fs.common.annotation.Log;
+import com.fs.common.constant.UserConstants;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyRole;
 import com.fs.company.service.ICompanyRoleService;
+import com.fs.framework.web.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Date;
 import java.util.List;
 
 /**
- * 角色信息Controller
+ * 销售角色权限Controller
+ *
+ * 平台端管理销售公司角色及菜单权限分配
  *
  * @author fs
  * @date 2021-10-04
@@ -27,6 +34,9 @@ public class CompanyRoleController extends BaseController
     @Autowired
     private ICompanyRoleService companyRoleService;
 
+    @Autowired
+    private TokenService tokenService;
+
     /**
      * 查询角色信息列表
      */
@@ -34,6 +44,8 @@ public class CompanyRoleController extends BaseController
     @GetMapping("/list")
     public TableDataInfo list(CompanyRole companyRole)
     {
+        // 平台端仅查询管理员设置的角色
+        companyRole.setIsAdmin("1");
         startPage();
         List<CompanyRole> list = companyRoleService.selectCompanyRoleList(companyRole);
         return getDataTable(list);
@@ -43,10 +55,12 @@ public class CompanyRoleController extends BaseController
      * 导出角色信息列表
      */
     @PreAuthorize("@ss.hasPermi('company:companyRole:export')")
-    @Log(title = "角色信息", businessType = BusinessType.EXPORT)
+    @Log(title = "销售角色信息", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(CompanyRole companyRole)
     {
+        // 平台端仅导出管理员设置的角色
+        companyRole.setIsAdmin("1");
         List<CompanyRole> list = companyRoleService.selectCompanyRoleList(companyRole);
         ExcelUtil<CompanyRole> util = new ExcelUtil<CompanyRole>(CompanyRole.class);
         return util.exportExcel(list, "companyRole");
@@ -63,35 +77,103 @@ public class CompanyRoleController extends BaseController
     }
 
     /**
-     * 新增角色信息
+     * 新增角色信息(含菜单权限分配)
      */
     @PreAuthorize("@ss.hasPermi('company:companyRole:add')")
-    @Log(title = "角色信息", businessType = BusinessType.INSERT)
+    @Log(title = "销售角色信息", businessType = BusinessType.INSERT)
     @PostMapping
-    public AjaxResult add(@RequestBody CompanyRole companyRole)
+    public AjaxResult add(@Validated @RequestBody CompanyRole companyRole)
     {
-        return toAjax(companyRoleService.insertCompanyRole(companyRole));
+        if (UserConstants.NOT_UNIQUE.equals(companyRoleService.checkRoleNameUnique(companyRole)))
+        {
+            return AjaxResult.error("新增角色'" + companyRole.getRoleName() + "'失败,角色名称已存在");
+        }
+        else if (UserConstants.NOT_UNIQUE.equals(companyRoleService.checkRoleKeyUnique(companyRole)))
+        {
+            return AjaxResult.error("新增角色'" + companyRole.getRoleName() + "'失败,角色权限已存在");
+        }
+        // 平台端新增角色:标记管理员设置,不绑定销售公司
+        companyRole.setIsAdmin("1");
+        companyRole.setCompanyId(null);
+        companyRole.setCreateTime(new Date());
+        return toAjax(companyRoleService.insertRole(companyRole));
     }
 
     /**
-     * 修改角色信息
+     * 修改角色信息(含菜单权限重建)
      */
     @PreAuthorize("@ss.hasPermi('company:companyRole:edit')")
-    @Log(title = "角色信息", businessType = BusinessType.UPDATE)
+    @Log(title = "销售角色信息", businessType = BusinessType.UPDATE)
     @PutMapping
-    public AjaxResult edit(@RequestBody CompanyRole companyRole)
+    public AjaxResult edit(@Validated @RequestBody CompanyRole companyRole)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(companyRoleService.checkRoleNameUnique(companyRole)))
+        {
+            return AjaxResult.error("修改角色'" + companyRole.getRoleName() + "'失败,角色名称已存在");
+        }
+        else if (UserConstants.NOT_UNIQUE.equals(companyRoleService.checkRoleKeyUnique(companyRole)))
+        {
+            return AjaxResult.error("修改角色'" + companyRole.getRoleName() + "'失败,角色权限已存在");
+        }
+        companyRole.setUpdateTime(new Date());
+        // 平台端角色不绑定销售公司
+        companyRole.setIsAdmin("1");
+        companyRole.setCompanyId(null);
+        if (companyRoleService.updateRole(companyRole) > 0)
+        {
+            return AjaxResult.success();
+        }
+        return AjaxResult.error("修改角色'" + companyRole.getRoleName() + "'失败,请联系管理员");
+    }
+
+    /**
+     * 修改角色状态
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyRole:edit')")
+    @Log(title = "销售角色信息", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody CompanyRole companyRole)
     {
-        return toAjax(companyRoleService.updateCompanyRole(companyRole));
+        CompanyRole existing = companyRoleService.selectCompanyRoleById(companyRole.getRoleId());
+        if (existing != null && "admin".equals(existing.getRoleKey()))
+        {
+            return AjaxResult.error("管理员角色不能修改状态");
+        }
+        companyRole.setUpdateTime(new Date());
+        return toAjax(companyRoleService.updateRoleStatus(companyRole));
     }
 
     /**
-     * 删除角色信息
+     * 删除角色信息(校验是否已分配用户)
      */
     @PreAuthorize("@ss.hasPermi('company:companyRole:remove')")
-    @Log(title = "角色信息", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{roleIds}")
+    @Log(title = "销售角色信息", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{roleIds}")
     public AjaxResult remove(@PathVariable Long[] roleIds)
     {
-        return toAjax(companyRoleService.deleteCompanyRoleByIds(roleIds));
+        return toAjax(companyRoleService.deleteRoleByIds(roleIds));
+    }
+
+    /**
+     * 获取角色选择框列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyRole:query')")
+    @GetMapping("/optionselect")
+    public AjaxResult optionselect()
+    {
+        return AjaxResult.success(companyRoleService.selectCompanyRoleList(new CompanyRole()));
+    }
+
+    /**
+     * 平台管理员角色下拉(is_admin=1,无分页,轻量)
+     */
+    @PreAuthorize("@ss.hasPermi('his:company:edit')")
+    @GetMapping("/adminOptionselect")
+    public AjaxResult adminOptionselect()
+    {
+        CompanyRole query = new CompanyRole();
+        query.setIsAdmin("1");
+        query.setStatus("0");
+        return AjaxResult.success(companyRoleService.selectCompanyRoleList(query));
     }
 }

+ 25 - 0
fs-admin/src/main/java/com/fs/his/controller/FsCompanyController.java

@@ -316,6 +316,31 @@ public class FsCompanyController extends BaseController
         return companyService.oneClickEnable(param.getCompanyIds(),loginUser);
     }
 
+    /**
+     * 查询公司当前管理员角色
+     */
+    @PreAuthorize("@ss.hasPermi('his:company:edit')")
+    @GetMapping("/adminRole/{companyId}")
+    public AjaxResult getAdminRole(@PathVariable("companyId") Long companyId)
+    {
+        return AjaxResult.success(companyService.getCompanyAdminRoleInfo(companyId));
+    }
+
+    /**
+     * 设置公司管理员角色(选项来自平台新增的 is_admin=1 角色)
+     */
+    @PreAuthorize("@ss.hasPermi('his:company:edit')")
+    @Log(title = "设置公司管理员角色", businessType = BusinessType.UPDATE)
+    @PutMapping("/setAdminRole")
+    public R setAdminRole(@RequestBody com.fs.company.param.CompanyAdminRoleParam param)
+    {
+        if (param == null)
+        {
+            return R.error("参数不能为空");
+        }
+        return companyService.setCompanyAdminRole(param.getCompanyId(), param.getRoleId());
+    }
+
     /**
      * 校验是否总公司部门
      * **/

+ 1 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductScrmController.java

@@ -292,7 +292,7 @@ public class FsStoreProductScrmController extends BaseController {
     /**
      * 同步商品到580商城
      */
-    @PreAuthorize("@ss.hasPermi('store:storeProduct:update')")
+    @PreAuthorize("@ss.hasPermi('store:storeProduct:updateIsShow')")
     @Log(title = "批量下架商品", businessType = BusinessType.DELETE, isStoreLog = true,
             logParamExpression = "#p0.length>1?new String[]{'商品','下架'}: new String[]{'商品','下架'}")
     @PostMapping("/updateFsStoreProductIsShowBatch/{productIds}")

+ 14 - 0
fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java

@@ -577,6 +577,20 @@ public class MallStoreTask
         fsUserOnlineStateService.insertUserNotOnline();
     }*/
 
+    @Autowired
+    private IFsStoreProductScrmService fsStoreProductScrmService;
+
+    /**
+     * 资质过期自动下架商品(建议每天执行一次)。
+     * 规则:上架中的药品,营业执照/生产许可证/注册证任一到期且非长期有效 → 下架并写入 offline_remark。
+     * 调度配置示例:调用目标 mallStoreTask.offlineExpiredQualificationProduct
+     */
+    public void offlineExpiredQualificationProduct() {
+        log.info("资质过期自动下架任务开始... 当前时间: {}", LocalTime.now());
+        int count = fsStoreProductScrmService.offlineExpiredQualificationProducts();
+        log.info("资质过期自动下架任务结束, 成功下架数量={}", count);
+    }
+
     /**
      * 提醒证件到期任务
      * */

+ 20 - 0
fs-admin/src/main/java/com/fs/web/controller/system/SysUserController.java

@@ -102,6 +102,26 @@ public class SysUserController extends BaseController
     public AjaxResult export(SysUser user)
     {
         List<SysUser> list = userService.selectUserList(user);
+        if (list != null)
+        {
+            for (SysUser item : list)
+            {
+                StringBuilder area = new StringBuilder();
+                if (StringUtils.isNotEmpty(item.getProvince()))
+                {
+                    area.append(item.getProvince());
+                }
+                if (StringUtils.isNotEmpty(item.getCity()))
+                {
+                    area.append(item.getCity());
+                }
+                if (StringUtils.isNotEmpty(item.getDistrict()))
+                {
+                    area.append(item.getDistrict());
+                }
+                item.setArea(area.toString());
+            }
+        }
         ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
         return util.exportExcel(list, "用户数据");
     }

+ 71 - 1
fs-common/src/main/java/com/fs/common/core/domain/entity/SysUser.java

@@ -98,6 +98,22 @@ public class SysUser extends BaseEntity
     @Excel(name = "角色名称")
     private List<String> roleName;
 
+    /** 省市区编码(逗号分隔,对应 fs_city.city_id) */
+    private String cityIds;
+
+    /** 省 */
+    private String province;
+
+    /** 市 */
+    private String city;
+
+    /** 区 */
+    private String district;
+
+    /** 区域(导出:省市区拼接) */
+    @Excel(name = "区域", type = Type.EXPORT)
+    private String area;
+
     public SysUser()
     {
 
@@ -324,7 +340,57 @@ public class SysUser extends BaseEntity
         this.roleId = roleId;
     }
 
-    @Override
+    
+
+    public String getArea()
+    {
+        return area;
+    }
+
+    public void setArea(String area)
+    {
+        this.area = area;
+    }
+    public String getCityIds()
+    {
+        return cityIds;
+    }
+
+    public void setCityIds(String cityIds)
+    {
+        this.cityIds = cityIds;
+    }
+
+    public String getProvince()
+    {
+        return province;
+    }
+
+    public void setProvince(String province)
+    {
+        this.province = province;
+    }
+
+    public String getCity()
+    {
+        return city;
+    }
+
+    public void setCity(String city)
+    {
+        this.city = city;
+    }
+
+    public String getDistrict()
+    {
+        return district;
+    }
+
+    public void setDistrict(String district)
+    {
+        this.district = district;
+    }
+@Override
     public String toString() {
         return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
             .append("userId", getUserId())
@@ -347,6 +413,10 @@ public class SysUser extends BaseEntity
             .append("updateTime", getUpdateTime())
             .append("remark", getRemark())
             .append("dept", getDept())
+            .append("cityIds", getCityIds())
+            .append("province", getProvince())
+            .append("city", getCity())
+            .append("district", getDistrict())
             .toString();
     }
 }

+ 45 - 0
fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java

@@ -5,6 +5,8 @@ import cn.hutool.json.JSONUtil;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.app.annotation.Login;
 import com.fs.app.config.ImageStorageConfig;
+import com.fs.app.param.FsAppBindLinkParam;
+import com.fs.app.param.FsAppBindUuidParam;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
@@ -434,4 +436,47 @@ public class FsUserCourseVideoController extends AppBaseController {
         }
     }
 
+
+    /**
+     * 获取APP好友注册绑定链接
+     * <p>将 companyId/companyUserId/projectId 以 JSON 写入 Redis(APPBIND:雪花ID,8小时过期),
+     * 返回「APP好友注册链接域名 + APPBIND:雪花ID」。</p>
+     */
+    @Login
+    @PostMapping("/getBindLink")
+    @ApiOperation("获取APP好友注册绑定链接")
+    public ResponseResult<String> getBindLink(@RequestBody FsAppBindLinkParam param) {
+        if (param == null) {
+            return ResponseResult.fail(500, "参数不能为空");
+        }
+        if (param.getCompanyId() == null || param.getCompanyUserId() == null || param.getProjectId() == null) {
+            return ResponseResult.fail(500, "companyId、companyUserId、projectId不能为空");
+        }
+        try {
+            String link = courseLinkService.getBindLink(param.getCompanyId(), param.getCompanyUserId(), param.getProjectId());
+            return ResponseResult.ok(link);
+        } catch (Exception e) {
+            log.error("获取APP好友注册绑定链接失败", e);
+            return ResponseResult.fail(500, e.getMessage());
+        }
+    }
+
+    /**
+     * 根据 uuid 获取绑定信息(从 Redis APPBIND:uuid 读取,无需登录)
+     */
+    @PostMapping("/getBindInfoByUuid")
+    @ApiOperation("根据uuid获取APP好友注册绑定信息")
+    public ResponseResult<Map<String, Object>> getBindInfoByUuid(@RequestBody FsAppBindUuidParam param) {
+        if (param == null || StringUtils.isEmpty(param.getUuid())) {
+            return ResponseResult.fail(500, "uuid不能为空");
+        }
+        try {
+            Map<String, Object> info = courseLinkService.getBindInfoByUuid(param.getUuid());
+            return ResponseResult.ok(info);
+        } catch (Exception e) {
+            log.error("根据uuid获取绑定信息失败", e);
+            return ResponseResult.fail(500, e.getMessage());
+        }
+    }
+
 }

+ 26 - 0
fs-company-app/src/main/java/com/fs/app/param/FsAppBindLinkParam.java

@@ -0,0 +1,26 @@
+package com.fs.app.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * APP\u597d\u53cb\u6ce8\u518c\u7ed1\u5b9a\u94fe\u63a5\u53c2\u6570
+ */
+@Data
+@ApiModel("APP\u597d\u53cb\u6ce8\u518c\u7ed1\u5b9a\u94fe\u63a5\u53c2\u6570")
+public class FsAppBindLinkParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "\u516c\u53f8ID", required = true)
+    private Long companyId;
+
+    @ApiModelProperty(value = "\u5458\u5de5ID", required = true)
+    private Long companyUserId;
+
+    @ApiModelProperty(value = "\u9879\u76eeID", required = true)
+    private Long projectId;
+}

+ 20 - 0
fs-company-app/src/main/java/com/fs/app/param/FsAppBindUuidParam.java

@@ -0,0 +1,20 @@
+package com.fs.app.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * \u6839\u636euuid\u67e5\u8be2APP\u597d\u53cb\u6ce8\u518c\u7ed1\u5b9a\u4fe1\u606f\u53c2\u6570
+ */
+@Data
+@ApiModel("\u6839\u636euuid\u67e5\u8be2\u7ed1\u5b9a\u4fe1\u606f\u53c2\u6570")
+public class FsAppBindUuidParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "\u7ed1\u5b9a\u94fe\u63a5\u96ea\u82b1ID\uff08\u5bf9\u5e94Redis Key\uff1aAPPBIND:uuid\uff09", required = true)
+    private String uuid;
+}

+ 2 - 1
fs-company-app/src/main/resources/application.yml

@@ -6,4 +6,5 @@ server:
 spring:
   profiles:
 #    active: druid-fcky-test
-    active: dev-yjb
+    #    active: dev-yjb
+    active: druid-yjb-test #医健宝测试库

+ 49 - 2
fs-company/src/main/java/com/fs/company/controller/company/CompanyLoginController.java

@@ -9,6 +9,7 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.company.domain.CompanyMenu;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyMenuService;
+import com.fs.company.service.ICompanyRoleService;
 import com.fs.framework.security.LoginBody;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.CompanyLoginService;
@@ -39,6 +40,9 @@ public class CompanyLoginController
     @Autowired
     private ICompanyMenuService menuService;
 
+    @Autowired
+    private ICompanyRoleService roleService;
+
     @Autowired
     private CompanyPermissionService permissionService;
 
@@ -73,6 +77,7 @@ public class CompanyLoginController
 
     /**
      * 获取用户信息
+     * 销售管理员:未绑定平台角色 → 全部权限;已绑定 is_admin=1 平台角色 → 按该角色权限
      *
      * @return 用户信息
      */
@@ -85,6 +90,9 @@ public class CompanyLoginController
         Set<String> roles = permissionService.getRolePermission(user);
         // 权限集合
         Set<String> permissions = permissionService.getMenuPermission(user);
+        // 同步刷新当前登录态权限,避免绑定平台角色后仍按旧 *:*:* 鉴权
+        loginUser.setPermissions(permissions);
+        tokenService.setLoginUser(loginUser);
         AjaxResult ajax = AjaxResult.success();
         ajax.put("user", user);
         ajax.put("roles", roles);
@@ -96,6 +104,7 @@ public class CompanyLoginController
 
     /**
      * 获取路由信息
+     * 销售管理员:未绑定平台角色 → 全部菜单;已绑定 → 平台角色菜单
      *
      * @return 路由信息
      */
@@ -103,9 +112,47 @@ public class CompanyLoginController
     public AjaxResult getRouters()
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        // 用户信息
         CompanyUser user = loginUser.getUser();
-        List<CompanyMenu> menus = menuService.selectMenuTreeByUserId(user.getUserId(), loginUser.getUser().isAdmin());
+        List<CompanyMenu> menus;
+        if (user.isAdmin() && roleService.hasPlatformAdminRole(user.getUserId()))
+        {
+            menus = menuService.selectPlatformPermMenuTree(user);
+        }
+        else
+        {
+            menus = menuService.selectMenuTreeByUserId(user.getUserId(), user.isAdmin());
+        }
+        return AjaxResult.success(menuService.buildMenus(menus));
+    }
+
+    /**
+     * 平台角色权限信息(新接口)
+     * 管理员须绑定 is_admin=1 平台角色才返回 roles/permissions;未绑定返回空
+     */
+    @GetMapping("getPlatformInfo")
+    public AjaxResult getPlatformInfo()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        CompanyUser user = loginUser.getUser();
+        Set<String> roles = permissionService.getPlatformRolePermission(user);
+        Set<String> permissions = permissionService.getPlatformMenuPermission(user);
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("user", user);
+        ajax.put("roles", roles);
+        ajax.put("permissions", permissions);
+        return ajax;
+    }
+
+    /**
+     * 平台角色路由(新接口)
+     * 管理员须绑定 is_admin=1 平台角色才返回路由;未绑定返回空
+     */
+    @GetMapping("getPlatformRouters")
+    public AjaxResult getPlatformRouters()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        CompanyUser user = loginUser.getUser();
+        List<CompanyMenu> menus = menuService.selectPlatformPermMenuTree(user);
         return AjaxResult.success(menuService.buildMenus(menus));
     }
 

+ 27 - 4
fs-company/src/main/java/com/fs/company/controller/company/CompanyMenuController.java

@@ -23,7 +23,6 @@ import java.util.List;
 /**
  * 菜单信息
  *
-
  */
 @RestController
 @RequestMapping("/company/companyMenu")
@@ -44,7 +43,7 @@ public class CompanyMenuController extends BaseController
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
-        List<CompanyMenu> menus = menuService.selectMenuList(menu, userId,loginUser.getUser().getUserType());
+        List<CompanyMenu> menus = menuService.selectMenuList(menu, userId, loginUser.getUser().getUserType());
         return AjaxResult.success(menus);
     }
 
@@ -66,7 +65,7 @@ public class CompanyMenuController extends BaseController
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
-        List<CompanyMenu> menus = menuService.selectMenuList(menu, userId,loginUser.getUser().getUserType());
+        List<CompanyMenu> menus = menuService.selectMenuList(menu, userId, loginUser.getUser().getUserType());
         return AjaxResult.success(menuService.buildMenuTreeSelect(menus));
     }
 
@@ -78,13 +77,37 @@ public class CompanyMenuController extends BaseController
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         CompanyMenu companyMenu = new CompanyMenu();
-        List<CompanyMenu> menus = menuService.selectMenuList(companyMenu,loginUser.getUser().getUserId(),loginUser.getUser().getUserType());
+        List<CompanyMenu> menus = menuService.selectMenuList(companyMenu, loginUser.getUser().getUserId(), loginUser.getUser().getUserType());
         AjaxResult ajax = AjaxResult.success();
         ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId));
         ajax.put("menus", menuService.buildMenuTreeSelect(menus));
         return ajax;
     }
 
+    /**
+     * 平台角色权限菜单查询(新接口)
+     * 管理员须绑定 is_admin=1 平台角色,按该角色权限返回;未绑定返回空
+     * 非管理员按原角色菜单返回
+     */
+    @GetMapping("/platformPermMenus")
+    public AjaxResult platformPermMenus()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return AjaxResult.success(menuService.selectPlatformPermMenus(loginUser.getUser()));
+    }
+
+    /**
+     * 平台角色权限菜单树(新接口)
+     * 管理员须绑定 is_admin=1 平台角色,按该角色权限返回树;未绑定返回空
+     */
+    @GetMapping("/platformPermTreeselect")
+    public AjaxResult platformPermTreeselect()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CompanyMenu> menus = menuService.selectPlatformPermMenus(loginUser.getUser());
+        return AjaxResult.success(menuService.buildMenuTreeSelect(menus));
+    }
+
     /**
      * 新增菜单
      */

+ 2 - 1
fs-company/src/main/java/com/fs/company/controller/company/CompanyUserBindWxController.java

@@ -11,6 +11,7 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import com.fs.his.domain.FsUserBindWx;
 import com.fs.his.domain.vo.*;
 import com.fs.his.service.IFsUserBindWxService;
 import com.fs.his.utils.PhoneUtil;
@@ -89,7 +90,7 @@ public class CompanyUserBindWxController extends BaseController {
     public R bindWxUser(@Valid @RequestBody BindWxUserParam param) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         // 从绑定记录获取销售人员ID
-        com.fs.his.domain.FsUserBindWx bindWx = fsUserBindWxService.selectFsUserBindWxById(param.getBindId());
+        FsUserBindWx bindWx = fsUserBindWxService.selectFsUserBindWxById(param.getBindId());
         if (bindWx == null) {
             return R.error("绑定记录不存在");
         }

+ 2 - 2
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -99,8 +99,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                 // 过滤请求
                 .authorizeRequests()
-                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
-                .antMatchers("/chat/upload/**","/login", "/register", "/captchaImage").anonymous()
+                // 登录/注册/验证码:permitAll(勿用 anonymous;带旧 Token 时 anonymous 会 403)
+                .antMatchers("/chat/upload/**", "/login", "/register", "/captchaImage").permitAll()
                 .antMatchers(
                         HttpMethod.GET,
                         "/",

+ 92 - 8
fs-company/src/main/java/com/fs/framework/service/CompanyPermissionService.java

@@ -1,18 +1,21 @@
 package com.fs.framework.service;
 
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyMenu;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyMenuService;
 import com.fs.company.service.ICompanyRoleService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
+import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 /**
  * 用户权限处理
- * 
- 
+ *
  */
 @Component
 public class CompanyPermissionService
@@ -24,17 +27,24 @@ public class CompanyPermissionService
 
     /**
      * 获取角色数据权限
-     * 
+     * 销售管理员:未绑定平台角色(is_admin=1) → admin;已绑定 → 平台角色标识
+     *
      * @param user 用户信息
      * @return 角色权限信息
      */
     public Set<String> getRolePermission(CompanyUser user)
     {
         Set<String> roles = new HashSet<String>();
-        // 管理员拥有所有权限
         if (user.isAdmin())
         {
-            roles.add("admin");
+            if (roleService.hasPlatformAdminRole(user.getUserId()))
+            {
+                roles.addAll(roleService.selectPlatformAdminRolePermissionByUserId(user.getUserId()));
+            }
+            else
+            {
+                roles.add("admin");
+            }
         }
         else
         {
@@ -45,17 +55,24 @@ public class CompanyPermissionService
 
     /**
      * 获取菜单数据权限
-     * 
+     * 销售管理员:未绑定平台角色(is_admin=1) → *:*:*;已绑定 → 平台角色菜单权限
+     *
      * @param user 用户信息
      * @return 菜单权限信息
      */
     public Set<String> getMenuPermission(CompanyUser user)
     {
         Set<String> perms = new HashSet<String>();
-        // 管理员拥有所有权限
         if (user.isAdmin())
         {
-            perms.add("*:*:*");
+            if (roleService.hasPlatformAdminRole(user.getUserId()))
+            {
+                perms.addAll(collectPlatformMenuPerms(user));
+            }
+            else
+            {
+                perms.add("*:*:*");
+            }
         }
         else
         {
@@ -63,4 +80,71 @@ public class CompanyPermissionService
         }
         return perms;
     }
+
+    /**
+     * 平台角色权限查询(新接口配套)
+     * 管理员须绑定 is_admin=1 平台角色才返回权限;未绑定返回空
+     * 非管理员按原角色权限返回
+     */
+    public Set<String> getPlatformMenuPermission(CompanyUser user)
+    {
+        Set<String> perms = new HashSet<String>();
+        if (user == null || user.getUserId() == null)
+        {
+            return perms;
+        }
+        if (user.isAdmin())
+        {
+            if (roleService.hasPlatformAdminRole(user.getUserId()))
+            {
+                perms.addAll(collectPlatformMenuPerms(user));
+            }
+            return perms;
+        }
+        perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
+        return perms;
+    }
+
+    /**
+     * 平台角色标识查询(新接口配套)
+     */
+    public Set<String> getPlatformRolePermission(CompanyUser user)
+    {
+        Set<String> roles = new HashSet<String>();
+        if (user == null || user.getUserId() == null)
+        {
+            return roles;
+        }
+        if (user.isAdmin())
+        {
+            if (roleService.hasPlatformAdminRole(user.getUserId()))
+            {
+                roles.addAll(roleService.selectPlatformAdminRolePermissionByUserId(user.getUserId()));
+            }
+            return roles;
+        }
+        roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));
+        return roles;
+    }
+
+    /**
+     * 从平台角色(is_admin=1)菜单中提取权限标识
+     */
+    private Set<String> collectPlatformMenuPerms(CompanyUser user)
+    {
+        Set<String> perms = new HashSet<String>();
+        List<CompanyMenu> menus = menuService.selectPlatformPermMenus(user);
+        if (menus == null || menus.isEmpty())
+        {
+            return perms;
+        }
+        for (CompanyMenu menu : menus)
+        {
+            if (menu != null && StringUtils.isNotEmpty(menu.getPerms()))
+            {
+                perms.addAll(Arrays.asList(menu.getPerms().trim().split(",")));
+            }
+        }
+        return perms;
+    }
 }

+ 2 - 1
fs-company/src/main/resources/application.yml

@@ -11,4 +11,5 @@ spring:
 #    active: druid-yzt
 #    active: druid-myhk
 #    active: druid-sft
-    active: dev-yjb
+    #    active: dev-yjb
+    active: druid-yjb-test #医健宝测试库

+ 11 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyRole.java

@@ -47,6 +47,8 @@ public class CompanyRole extends BaseEntity
     /** 删除标志(0代表存在 2代表删除) */
     private String delFlag;
 
+    /** 是否管理员设置(0否 1是;平台端新增为1,默认0) */
+    private String isAdmin;
 
     /** 菜单组 */
     private Long[] menuIds;
@@ -147,6 +149,15 @@ public class CompanyRole extends BaseEntity
         return delFlag;
     }
 
+    public String getIsAdmin()
+    {
+        return isAdmin;
+    }
+
+    public void setIsAdmin(String isAdmin)
+    {
+        this.isAdmin = isAdmin;
+    }
 
     public static long getSerialVersionUID() {
         return serialVersionUID;

+ 5 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyMenuMapper.java

@@ -67,6 +67,11 @@ public interface CompanyMenuMapper
 
     List<CompanyMenu> selectMenuTreeByUserId(Long userId);
 
+    /**
+     * 按用户绑定角色查询菜单列表(含按钮,优先平台 is_admin=1 角色)
+     */
+    List<CompanyMenu> selectMenuListByUserId(@Param("userId") Long userId);
+
     List<Integer> selectMenuListByRoleId(@Param("roleId") Long roleId,@Param("menuCheckStrictly") boolean menuCheckStrictly);
 
     int hasChildByMenuId(Long menuId);

+ 23 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyRoleMapper.java

@@ -2,6 +2,7 @@ package com.fs.company.mapper;
 
 import com.fs.company.domain.CompanyRole;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 import java.util.List;
 
@@ -85,4 +86,26 @@ public interface CompanyRoleMapper
      * **/
     CompanyRole selectCompanyRoleByRoleKey(@Param("roleKey") String roleKey);
     Long selectRolesByUserNameAndCompanyId(@Param("roleName") String roleName,@Param("companyId") Long companyId);
+
+    /**
+     * 判断用户是否绑定了平台设置的管理员角色(is_admin=1)
+     */
+    @Select("SELECT COUNT(1) FROM company_user_role ur " +
+            "INNER JOIN company_role r ON ur.role_id = r.role_id " +
+            "WHERE ur.user_id = #{userId} " +
+            "AND r.is_admin = '1' " +
+            "AND IFNULL(r.status, '0') = '0' " +
+            "AND IFNULL(r.del_flag, '0') = '0'")
+    int countPlatformAdminRoleByUserId(@Param("userId") Long userId);
+
+    /**
+     * 查询用户绑定的平台管理员角色 role_key(is_admin=1)
+     */
+    @Select("SELECT DISTINCT r.role_key FROM company_user_role ur " +
+            "INNER JOIN company_role r ON ur.role_id = r.role_id " +
+            "WHERE ur.user_id = #{userId} " +
+            "AND r.is_admin = '1' " +
+            "AND IFNULL(r.status, '0') = '0' " +
+            "AND IFNULL(r.del_flag, '0') = '0'")
+    List<String> selectPlatformAdminRoleKeysByUserId(@Param("userId") Long userId);
 }

+ 13 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyUserRoleMapper.java

@@ -79,4 +79,17 @@ public interface CompanyUserRoleMapper
     void batchUserRole(List<CompanyUserRole> list);
 
     void deleteUserRoleByUserId(Long userId);
+
+    /**
+     * 按公司查询当前绑定的管理员角色(优先 is_admin=1)
+     */
+    @Select("SELECT c.company_id AS companyId, c.company_name AS companyName, c.user_id AS userId, " +
+            "r.role_id AS roleId, r.role_name AS roleName, r.is_admin AS isAdmin " +
+            "FROM company c " +
+            "LEFT JOIN company_user_role ur ON ur.user_id = c.user_id " +
+            "LEFT JOIN company_role r ON r.role_id = ur.role_id " +
+            "WHERE c.company_id = #{companyId} " +
+            "ORDER BY CASE WHEN r.is_admin = '1' THEN 0 ELSE 1 END, r.role_id ASC " +
+            "LIMIT 1")
+    java.util.Map<String, Object> selectAdminRoleInfoByCompanyId(@Param("companyId") Long companyId);
 }

+ 33 - 0
fs-service/src/main/java/com/fs/company/param/CompanyAdminRoleParam.java

@@ -0,0 +1,33 @@
+package com.fs.company.param;
+
+/**
+ * 设置公司管理员角色参数
+ */
+public class CompanyAdminRoleParam
+{
+    /** 公司ID */
+    private Long companyId;
+
+    /** 角色ID(平台新增的 is_admin=1 角色) */
+    private Long roleId;
+
+    public Long getCompanyId()
+    {
+        return companyId;
+    }
+
+    public void setCompanyId(Long companyId)
+    {
+        this.companyId = companyId;
+    }
+
+    public Long getRoleId()
+    {
+        return roleId;
+    }
+
+    public void setRoleId(Long roleId)
+    {
+        this.roleId = roleId;
+    }
+}

+ 9 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyMenuService.java

@@ -82,5 +82,14 @@ public interface ICompanyMenuService
 
     boolean hasChildByMenuId(Long menuId);
 
+    /**
+     * 平台角色权限菜单列表(管理员+is_admin=1;否则空)
+     */
+    List<CompanyMenu> selectPlatformPermMenus(com.fs.company.domain.CompanyUser user);
+
+    /**
+     * 平台角色侧边栏菜单树(管理员+is_admin=1;否则空)
+     */
+    List<CompanyMenu> selectPlatformPermMenuTree(com.fs.company.domain.CompanyUser user);
 
 }

+ 10 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyRoleService.java

@@ -109,4 +109,14 @@ public interface ICompanyRoleService
      * @return
      * **/
     int insertDefaultRole(CompanyRole role);
+
+    /**
+     * 判断用户是否绑定了平台设置的管理员角色(is_admin=1)
+     */
+    boolean hasPlatformAdminRole(Long userId);
+
+    /**
+     * 查询用户绑定的平台管理员角色权限标识(is_admin=1)
+     */
+    java.util.Collection<? extends String> selectPlatformAdminRolePermissionByUserId(Long userId);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyService.java

@@ -207,4 +207,14 @@ public interface ICompanyService
      * @return boolean true:封禁 false:未封禁
      * **/
     boolean checkIpadDisable(Long companyId);
+
+    /**
+     * 查询公司当前管理员角色信息(优先返回 is_admin=1 的角色)
+     */
+    java.util.Map<String, Object> getCompanyAdminRoleInfo(Long companyId);
+
+    /**
+     * 设置公司管理员角色(绑定到公司管理员账号)
+     */
+    R setCompanyAdminRole(Long companyId, Long roleId);
 }

+ 46 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyMenuServiceImpl.java

@@ -241,6 +241,52 @@ public class CompanyMenuServiceImpl implements ICompanyMenuService
         return menuList;
     }
 
+    /**
+     * 平台角色权限菜单查询:
+     * 仅管理员且已绑定 is_admin=1 时,按平台角色菜单返回;否则返回空
+     * (非管理员请继续使用原 list/treeselect/getRouters 接口)
+     */
+    @Override
+    public List<CompanyMenu> selectPlatformPermMenus(CompanyUser user) {
+        if (user == null || user.getUserId() == null || !user.isAdmin())
+        {
+            return new ArrayList<CompanyMenu>();
+        }
+        if (roleMapper.countPlatformAdminRoleByUserId(user.getUserId()) <= 0)
+        {
+            return new ArrayList<CompanyMenu>();
+        }
+        return companyMenuMapper.selectMenuListByUserId(user.getUserId());
+    }
+
+    /**
+     * 平台角色侧边栏菜单树(仅 M/C,来自 is_admin=1 角色)
+     */
+    @Override
+    public List<CompanyMenu> selectPlatformPermMenuTree(CompanyUser user) {
+        if (user == null || user.getUserId() == null || !user.isAdmin())
+        {
+            return new ArrayList<CompanyMenu>();
+        }
+        if (roleMapper.countPlatformAdminRoleByUserId(user.getUserId()) <= 0)
+        {
+            return new ArrayList<CompanyMenu>();
+        }
+        List<CompanyMenu> menus = companyMenuMapper.selectMenuListByUserId(user.getUserId());
+        List<CompanyMenu> sideMenus = new ArrayList<CompanyMenu>();
+        if (menus != null)
+        {
+            for (CompanyMenu menu : menus)
+            {
+                if (menu != null && ("M".equals(menu.getMenuType()) || "C".equals(menu.getMenuType())))
+                {
+                    sideMenus.add(menu);
+                }
+            }
+        }
+        return getChildPerms(sideMenus, 0);
+    }
+
     @Override
     public List<CompanyMenuTreeSelect> buildMenuTreeSelect(List<CompanyMenu> menus) {
         List<CompanyMenu> menuTrees = buildMenuTree(menus);

+ 31 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyRoleServiceImpl.java

@@ -281,4 +281,35 @@ public class CompanyRoleServiceImpl implements ICompanyRoleService
     public int insertDefaultRole(CompanyRole role) {
         return companyRoleMapper.insertCompanyRole(role);
     }
+
+    @Override
+    public boolean hasPlatformAdminRole(Long userId) {
+        if (userId == null)
+        {
+            return false;
+        }
+        return companyRoleMapper.countPlatformAdminRoleByUserId(userId) > 0;
+    }
+
+    @Override
+    public Collection<? extends String> selectPlatformAdminRolePermissionByUserId(Long userId) {
+        Set<String> permsSet = new HashSet<>();
+        if (userId == null)
+        {
+            return permsSet;
+        }
+        List<String> roleKeys = companyRoleMapper.selectPlatformAdminRoleKeysByUserId(userId);
+        if (roleKeys == null || roleKeys.isEmpty())
+        {
+            return permsSet;
+        }
+        for (String roleKey : roleKeys)
+        {
+            if (StringUtils.isNotEmpty(roleKey))
+            {
+                permsSet.addAll(Arrays.asList(roleKey.trim().split(",")));
+            }
+        }
+        return permsSet;
+    }
 }

+ 66 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -1544,4 +1544,70 @@ public class CompanyServiceImpl implements ICompanyService
         }
     }
 
+    /**
+     * 查询公司当前管理员角色信息
+     */
+    @Override
+    public java.util.Map<String, Object> getCompanyAdminRoleInfo(Long companyId)
+    {
+        java.util.Map<String, Object> result = new java.util.HashMap<String, Object>();
+        result.put("companyId", companyId);
+        result.put("roleId", null);
+        result.put("roleName", null);
+        if (companyId == null)
+        {
+            return result;
+        }
+        java.util.Map<String, Object> info = userRoleMapper.selectAdminRoleInfoByCompanyId(companyId);
+        if (info == null || info.isEmpty())
+        {
+            return result;
+        }
+        result.putAll(info);
+        return result;
+    }
+
+    /**
+     * 设置公司管理员角色:将平台 is_admin=1 的角色绑定到公司管理员账号
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R setCompanyAdminRole(Long companyId, Long roleId)
+    {
+        if (companyId == null || roleId == null)
+        {
+            return R.error("公司ID和角色ID不能为空");
+        }
+        Company company = companyMapper.selectCompanyById(companyId);
+        if (company == null)
+        {
+            return R.error("公司不存在");
+        }
+        if (company.getUserId() == null)
+        {
+            return R.error("公司未配置管理员账号,无法设置角色");
+        }
+        CompanyRole role = roleMapper.selectCompanyRoleById(roleId);
+        if (role == null)
+        {
+            return R.error("角色不存在");
+        }
+        if (!"1".equals(role.getIsAdmin()))
+        {
+            return R.error("只能选择平台管理员设置的角色");
+        }
+        if (!"0".equals(role.getStatus()))
+        {
+            return R.error("角色已停用,无法设置");
+        }
+        // 平台角色不绑定公司,可直接赋给任意销售公司管理员
+        // 重建管理员账号的角色绑定
+        userRoleMapper.deleteUserRoleByUserId(company.getUserId());
+        CompanyUserRole userRole = new CompanyUserRole();
+        userRole.setUserId(company.getUserId());
+        userRole.setRoleId(roleId);
+        userRoleMapper.insertCompanyUserRole(userRole);
+        return R.ok("设置成功");
+    }
+
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/config/CourseConfig.java

@@ -21,6 +21,8 @@ public class CourseConfig implements Serializable {
     private String authDomainName;//网页授权域名
     private String mpAppId;//看课公众号APPID
     private String registerDomainName;//注册域名
+    /** APP好友注册链接域名 */
+    private String appFriendRegisterDomainName;
     private String courseDomainName;//链接域名
     private String miniprogramAppid;//链接域名
     private Integer rewardType; // 奖励类型 1红包 2积分 3红包+积分

+ 19 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseLinkService.java

@@ -7,6 +7,7 @@ import com.fs.course.param.FsCourseLinkRoomParam;
 import com.fs.qw.domain.QwUser;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 短链Service接口
@@ -98,4 +99,22 @@ public interface IFsCourseLinkService
     R getWxaCodeGenerateScheme(String linkStr,String appId);
 
     R getProjectCode();
+
+    /**
+     * 获取绑定链接
+     *
+     * @param companyId     公司ID
+     * @param companyUserId 销售ID
+     * @param projectId     项目ID
+     * @return APP好友注册链接域名 + ?uuid=雪花ID
+     */
+    String getBindLink(Long companyId, Long companyUserId, Long projectId);
+
+    /**
+     * 根据 uuid 从 Redis 读取绑定信息(与 getBindLink 存入规则一致:APPBIND:uuid)
+     *
+     * @param uuid 雪花ID,或带 APPBIND: 前缀的完整 key
+     * @return companyId、companyUserId、projectId
+     */
+    Map<String, Object> getBindInfoByUuid(String uuid);
 }

+ 58 - 12
fs-service/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java

@@ -1,22 +1,21 @@
 package com.fs.course.service.impl;
 
 import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.hutool.core.util.IdUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
-import com.fs.common.utils.date.DateUtil;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.config.cloud.CloudHostProper;
-import com.fs.core.config.WxMaConfig;
 import com.fs.core.config.WxMaConfiguration;
 import com.fs.course.config.CourseConfig;
-import com.fs.course.config.CourseMaConfig;
 import com.fs.course.domain.*;
 import com.fs.course.mapper.FsCourseDomainNameMapper;
 import com.fs.course.mapper.FsCourseLinkMapper;
@@ -27,10 +26,8 @@ import com.fs.course.param.FsCourseLinkRoomParam;
 import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.course.service.IFsUserCourseService;
-import com.fs.his.config.FsSysConfig;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.his.utils.HttpUtil;
-import com.fs.his.utils.RedisCacheUtil;
 import com.fs.qw.domain.QwGroupChat;
 import com.fs.qw.domain.QwGroupChatUser;
 import com.fs.qw.domain.QwUser;
@@ -39,7 +36,6 @@ import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwGroupChatService;
 import com.fs.qw.service.IQwGroupChatUserService;
 import com.fs.qw.vo.GroupUserExternalVo;
-import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.voice.utils.StringUtil;
@@ -47,13 +43,9 @@ import com.google.common.reflect.TypeToken;
 import com.google.gson.Gson;
 import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
-import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
 import me.chanjar.weixin.common.error.WxErrorException;
-import me.chanjar.weixin.mp.api.WxMpService;
-import org.apache.commons.lang3.ObjectUtils;
 import org.apache.http.HttpEntity;
 import org.apache.http.client.ClientProtocolException;
-import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.CloseableHttpClient;
@@ -69,14 +61,12 @@ import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
-import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 
 import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
-import static com.fs.sop.service.impl.SopUserLogsInfoServiceImpl.convertStringToDate;
 
 /**
  * 短链Service业务层处理
@@ -1097,4 +1087,60 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
         }
         return R.ok().put("code",code);
     }
+
+    @Override
+    public String getBindLink(Long companyId, Long companyUserId, Long projectId) {
+        if (companyId == null || companyUserId == null || projectId == null) {
+            throw new CustomException("companyId、companyUserId、projectId不能为空");
+        }
+        String json = configService.selectConfigByKey("course.config");
+        if (StringUtils.isEmpty(json)) {
+            throw new CustomException("点播配置不存在");
+        }
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        String domain = config.getAppFriendRegisterDomainName();
+        if (StringUtils.isEmpty(domain)) {
+            throw new CustomException("未配置APP好友注册链接域名");
+        }
+
+        // 雪花ID,Redis Key 形如 APPBIND:1938...
+        String snowflakeId = IdUtil.getSnowflake(0, 0).nextIdStr();
+        String redisKey = "APPBIND:" + snowflakeId;
+        Map<String, Object> cacheValue = new HashMap<>(4);
+        cacheValue.put("companyId", companyId);
+        cacheValue.put("companyUserId", companyUserId);
+        cacheValue.put("projectId", projectId);
+        redisCache.setCacheObject(redisKey, JSON.toJSONString(cacheValue), 8, TimeUnit.HOURS);
+
+        return domain + "?uuid=" + snowflakeId;
+    }
+
+    @Override
+    public Map<String, Object> getBindInfoByUuid(String uuid) {
+        if (StringUtils.isEmpty(uuid)) {
+            throw new CustomException("uuid不能为空");
+        }
+        String key = uuid.trim();
+        String redisKey = key.startsWith("APPBIND:") ? key : ("APPBIND:" + key);
+        String cacheData = redisCache.getCacheObject(redisKey);
+        if (StringUtils.isEmpty(cacheData)) {
+            throw new CustomException("绑定链接已失效或不存在");
+        }
+        JSONObject cacheJson = JSON.parseObject(cacheData);
+        if (cacheJson == null) {
+            throw new CustomException("绑定链接数据异常");
+        }
+        Long companyId = cacheJson.getLong("companyId");
+        Long companyUserId = cacheJson.getLong("companyUserId");
+        Long projectId = cacheJson.getLong("projectId");
+        if (companyId == null || companyUserId == null || projectId == null) {
+            throw new CustomException("绑定链接数据不完整");
+        }
+        Map<String, Object> result = new HashMap<>(4);
+        result.put("companyId", companyId);
+        result.put("companyUserId", companyUserId);
+        result.put("projectId", projectId);
+        result.put("uuid", key.startsWith("APPBIND:") ? key.substring("APPBIND:".length()) : key);
+        return result;
+    }
 }

+ 1 - 1
fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java

@@ -708,7 +708,7 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
             String combinationValue = company.getCompanyName();
             if(fsStoreOrder.getCompanyUserId() != null){
                 CompanyUser  companyUser = companyUserMapper.selectCompanyUserById(fsStoreOrder.getCompanyUserId());
-                shopOrderDTO.setRemark("销售员:"+companyUser.getUserName());
+                shopOrderDTO.setRemark("客服:"+companyUser.getUserName());
             }
 
            if(shopOrderDTO != null){

+ 13 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserService.java

@@ -98,6 +98,19 @@ public interface IFsUserService
      */
     public int deleteFsUserByUserId(Long userId);
 
+    /**
+     * 物理删除用户(真实 delete,非逻辑删除)
+     *
+     * @param userId 用户主键
+     * @return 结果
+     */
+    int physicalDeleteFsUserByUserId(Long userId);
+
+    /**
+     * 按手机号+业务类型查询用户列表
+     */
+    List<FsUser> selectFsUsersByPhoneAndBizType(String phone, Integer bizType);
+
     List<FsUserVO> selectFsUserListVO(FsUserParam fsUser);
 
     FsUser selectFsUserByOpenId(String openId);

+ 76 - 8
fs-service/src/main/java/com/fs/his/service/impl/FsUserBindWxServiceImpl.java

@@ -6,15 +6,21 @@ import com.fs.his.domain.FsImFriendship;
 import com.fs.his.domain.FsUserBindWx;
 import com.fs.his.domain.vo.*;
 import com.fs.his.mapper.FsUserBindWxMapper;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.common.utils.DateUtils;
 import com.fs.his.param.FsAddUserParam;
 import com.fs.his.service.IFsImFriendshipService;
 import com.fs.his.service.IFsUserBindWxService;
-import com.fs.his.utils.PhoneUtil;
 import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.im.service.OpenIMService;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import com.fs.common.utils.StringUtils;
+import com.fs.course.domain.FsUserCompanyUser;
+import com.fs.his.domain.FsUser;
+import com.fs.his.service.IFsUserService;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.stereotype.Service;
 
 import java.util.Arrays;
@@ -45,6 +51,12 @@ public class FsUserBindWxServiceImpl implements IFsUserBindWxService {
     @Autowired
     private IFsUserCompanyUserService fsUserCompanyUserService;
 
+    @Autowired
+    private IFsUserService fsUserService;
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
     @Override
     public FsUserBindWx selectFsUserBindWxById(Long id) {
         return fsUserBindWxMapper.selectFsUserBindWxById(id);
@@ -123,6 +135,7 @@ public class FsUserBindWxServiceImpl implements IFsUserBindWxService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void bindWxUser(Long bindId, Long bindUserId, Long companyUserId,
                            Long externalContactId, Long projectId, Integer type) {
         // 验证绑定信息是否存在
@@ -131,18 +144,54 @@ public class FsUserBindWxServiceImpl implements IFsUserBindWxService {
             throw new ServiceException("操作失败,绑定信息不存在!");
         }
 
+        Long oldFsUserId = bindWxInfo.getFsUserId();
+
         // 自动补全 type
         Integer bindType = type;
         if (bindType == null) {
             bindType = externalContactId != null ? 2 : 1;
         }
 
-        // 手动模式:插入 fs_user_company_user 记录
+        // 查询原会员与待绑定微信用户
+        FsUser oldFsUser = null;
+        FsUser bindFsUser = null;
+        if (oldFsUserId != null) {
+            oldFsUser = fsUserService.selectFsUserById(oldFsUserId);
+            if (oldFsUser == null) {
+                throw new ServiceException("原会员不存在!");
+            }
+        }
+        if (bindUserId != null) {
+            bindFsUser = fsUserService.selectFsUserById(bindUserId);
+            if (bindFsUser == null) {
+                throw new ServiceException("要绑定的微信用户不存在!");
+            }
+            // 将原会员手机号合并到绑定微信用户
+            // 注意:updateFsUser 仅接受11位数字明文,否则会把 phone 置为 null;库中一般是密文,需直接拷贝
+            if (oldFsUser != null && StringUtils.isNotEmpty(oldFsUser.getPhone())) {
+                String oldPhone = oldFsUser.getPhone();
+                FsUser updateUser = new FsUser();
+                updateUser.setUserId(bindUserId);
+                if (oldPhone.length() == 11 && oldPhone.matches("\\d+")) {
+                    updateUser.setPhone(oldPhone);
+                    fsUserService.updateFsUser(updateUser);
+                } else {
+                    updateUser.setPhone(oldPhone);
+                    updateUser.setUpdateTime(DateUtils.getNowDate());
+                    fsUserMapper.updateFsUser(updateUser);
+                }
+            }
+        }
+
+        // 手动模式:插入 fs_user_company_user 记录(绑定到 bindUserId)
         Long userCompanyUserId = null;
         if (externalContactId == null && projectId != null) {
-            com.fs.course.domain.FsUserCompanyUser ucUser =
+            if (bindUserId == null) {
+                throw new ServiceException("请选择要绑定的微信用户!");
+            }
+            FsUserCompanyUser ucUser =
                     fsUserCompanyUserService.bindRelationship(
-                            bindWxInfo.getFsUserId(), projectId,
+                            bindUserId, projectId,
                             bindWxInfo.getCompanyId(), companyUserId, 1);
             userCompanyUserId = ucUser.getId();
         }
@@ -162,7 +211,7 @@ public class FsUserBindWxServiceImpl implements IFsUserBindWxService {
             if (bindUserId != null) {
                 wxInfo = fsUserBindWxMapper.selectFsUserWxInfo(bindUserId);
             }
-            // 更新绑定记录
+            // 更新绑定记录(fs_user_id 同步改为 bindUserId)
             fsUserBindWxMapper.updateBindWxUser(bindId, bindUserId, companyUserId,
                     wxInfo != null ? wxInfo.getUnionId() : null,
                     wxInfo != null ? wxInfo.getMaOpenId() : null,
@@ -171,12 +220,14 @@ public class FsUserBindWxServiceImpl implements IFsUserBindWxService {
             // 更新外部联系人表的 fs_user_id
             com.fs.qw.domain.QwExternalContact updateEc = new com.fs.qw.domain.QwExternalContact();
             updateEc.setId(externalContactId);
-            updateEc.setFsUserId(bindWxInfo.getFsUserId());
+            updateEc.setFsUserId(bindUserId);
             qwExternalContactMapper.updateQwExternalContact(updateEc);
+            // 物理删除原会员及其销售关系(合并账号后不再保留)
+            physicalDeleteOldFsUser(oldFsUserId, bindUserId);
             // 同步IM好友关系
             if (bindUserId != null) {
                 FsAddUserParam addUserParam = new FsAddUserParam();
-                addUserParam.setUserIds(Arrays.asList(bindWxInfo.getFsUserId()));
+                addUserParam.setUserIds(Arrays.asList(bindUserId));
                 addUserParam.setAddType(1);
                 addUserParam.setCompanyUserId(companyUserId);
                 fsImFriendshipService.addUserAndSaleFriend(addUserParam);
@@ -196,14 +247,31 @@ public class FsUserBindWxServiceImpl implements IFsUserBindWxService {
                 wxInfo.getUnionId(), wxInfo.getMaOpenId(), wxInfo.getMpOpenId(),
                 null, userCompanyUserId, bindType);
 
+        // 物理删除原会员及其销售关系(合并账号后不再保留)
+        physicalDeleteOldFsUser(oldFsUserId, bindUserId);
+
         // 同步IM好友关系
         FsAddUserParam addUserParam = new FsAddUserParam();
-        addUserParam.setUserIds(Arrays.asList(bindWxInfo.getFsUserId()));
+        addUserParam.setUserIds(Arrays.asList(bindUserId));
         addUserParam.setAddType(1);
         addUserParam.setCompanyUserId(companyUserId);
         fsImFriendshipService.addUserAndSaleFriend(addUserParam);
     }
 
+    /**
+     * 物理删除原 fsUser 及其 fs_user_company_user(与绑定用户相同时跳过)
+     */
+    private void physicalDeleteOldFsUser(Long oldFsUserId, Long bindUserId) {
+        if (oldFsUserId == null || oldFsUserId.equals(bindUserId)) {
+            return;
+        }
+        fsUserCompanyUserService.remove(
+                Wrappers.<FsUserCompanyUser>lambdaQuery()
+                        .eq(FsUserCompanyUser::getUserId, oldFsUserId)
+        );
+        fsUserService.physicalDeleteFsUserByUserId(oldFsUserId);
+    }
+
     @Override
     public int unbindWxUser(Long id) {
         FsUserBindWx bindWx = fsUserBindWxMapper.selectFsUserBindWxById(id);

+ 10 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -293,6 +293,16 @@ public class FsUserServiceImpl implements IFsUserService {
         return fsUserMapper.updateFsUserByUserId(userId);
     }
 
+    @Override
+    public int physicalDeleteFsUserByUserId(Long userId) {
+        return fsUserMapper.deleteFsUserByUserId(userId);
+    }
+
+    @Override
+    public List<FsUser> selectFsUsersByPhoneAndBizType(String phone, Integer bizType) {
+        return fsUserMapper.selectFsUsersByPhoneLimitOne(phone, bizType);
+    }
+
     @Override
     public List<FsUserVO> selectFsUserListVO(FsUserParam fsUser) {
         return fsUserMapper.selectFsUserListVO(fsUser);

+ 1 - 1
fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java

@@ -108,6 +108,6 @@ public class PhoneUtil {
     }
 
     public static void main(String[] args) {
-        System.out.println(encryptPhone("13594768440"));
+        System.out.println(encryptPhone("13974873957"));
     }
 }

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java

@@ -614,4 +614,9 @@ public class FsStoreProductScrm extends BaseEntity {
      * **/
     private String specification;
 
+    /**
+     * 下架备注(如资质过期自动下架原因)
+     **/
+    private String offlineRemark;
+
 }

+ 15 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java

@@ -659,6 +659,21 @@ public interface FsStoreProductScrmMapper
      */
     List<String> getProductNoticeInfo(@Param("storeId") Long storeId);
 
+    /**
+     * 查询资质已过期且仍上架的药品商品
+     * @return 待下架商品列表
+     */
+    List<FsStoreProductScrm> selectExpiredQualificationProducts();
+
+    /**
+     * 资质过期自动下架(更新商品状态及下架备注)
+     * @param productId 商品ID
+     * @param offlineRemark 下架备注
+     * @return 影响行数
+     */
+    int offlineProductByQualification(@Param("productId") Long productId,
+                                      @Param("offlineRemark") String offlineRemark);
+
     /**
      * 获取店铺产品信息
      * @param ids 商品ID

+ 6 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java

@@ -193,6 +193,12 @@ public interface IFsStoreProductScrmService
      * **/
     List<String> getProductNoticeInfo(Long storeId);
 
+    /**
+     * 资质过期自动下架(营业执照/生产许可证/注册证,排除长期有效)
+     * @return 成功下架数量
+     */
+    int offlineExpiredQualificationProducts();
+
     /**
      * 复制商品
      * **/

+ 378 - 35
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -345,6 +345,285 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         return diff;
     }
 
+    /** 主表免审字段(与配置 productColumns 取并集) */
+    private static final Set<String> DEFAULT_PRODUCT_EXEMPT_AUDIT_FIELDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            "agentPrice", "cost", "otPrice", "stock", "integral",
+            "isHot", "isGood", "isDisplay", "isBest", "isNew",
+            "giveIntegral", "sort", "sales"
+    )));
+
+    /** 规格免审字段 */
+    private static final Set<String> DEFAULT_ATTR_EXEMPT_AUDIT_FIELDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            "agentPrice", "cost", "otPrice", "stock", "groupBarCode",
+            "weight", "volume", "integral",
+            "brokerage", "brokerageTwo", "brokerageThree",
+            "sales", "giveIntegral"
+    )));
+
+    /** 对比忽略字段 */
+    private static final Set<String> PRODUCT_AUDIT_IGNORE_FIELDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            "isAudit", "reviewAudit", "commentContent", "complaint",
+            "storeIds", "serialVersionUID", "browse", "views",
+            "codePath", "codeUrl", "transactionStatus", "platformProductId",
+            "updateTime", "createTime", "params", "searchValue",
+            "beginTime", "endTime", "createBy", "updateBy", "remark",
+            "id", "productId", "storeId", "detail"
+    )));
+
+    /**
+     * 已审通过商品编辑时是否需要重新待审。
+     * 规则:商品状态下架不触发审核;上架时再按字段差异判定(免审字段除外)。
+     */
+    private boolean needReAuditOnEdit(FsStoreProductScrm oldProduct, FsStoreProductScrm newProduct,
+                                      FsStoreProductAddEditParam param) {
+        if (oldProduct == null || newProduct == null) {
+            return false;
+        }
+        if (!"1".equals(oldProduct.getIsAudit())) {
+            log.info("product skip re-audit, not audited passed, productId={}, oldIsAudit={}",
+                    oldProduct.getProductId(), oldProduct.getIsAudit());
+            return false;
+        }
+        // 下架不需要审核;仅上架才进入字段差异审核
+        Integer newShow = newProduct.getIsShow();
+        if (newShow == null || newShow.intValue() != 1) {
+            log.info("product skip re-audit, offline, productId={}, newIsShow={}",
+                    oldProduct.getProductId(), newShow);
+            return false;
+        }
+        if (!isProductAuditSwitchOn()) {
+            log.info("product skip re-audit, audit switch off, productId={}", oldProduct.getProductId());
+            return false;
+        }
+        Set<String> exemptFields = resolveProductExemptAuditFields();
+        try {
+            for (String fieldName : collectChangedFields(oldProduct, newProduct)) {
+                if (PRODUCT_AUDIT_IGNORE_FIELDS.contains(fieldName) || exemptFields.contains(fieldName)) {
+                    continue;
+                }
+                log.info("product re-audit by main field, productId={}, field={}", oldProduct.getProductId(), fieldName);
+                return true;
+            }
+            boolean attrChanged = hasSensitiveAttrChange(oldProduct.getProductId(), param == null ? null : param.getValues());
+            if (attrChanged) {
+                log.info("product re-audit by attr change, productId={}", oldProduct.getProductId());
+            } else {
+                log.info("product no re-audit, no sensitive change, productId={}", oldProduct.getProductId());
+            }
+            return attrChanged;
+        } catch (Exception e) {
+            log.error("product audit diff failed, productId={}", oldProduct.getProductId(), e);
+            return false;
+        }
+    }
+
+    private boolean isProductAuditSwitchOn() {
+        try {
+            JSONObject switchConfig = configUtil.generateConfigByKey("medicalMall.func.switch");
+            if (switchConfig == null) {
+                return false;
+            }
+            return Boolean.TRUE.equals(switchConfig.getBoolean("isAudit"));
+        } catch (Exception e) {
+            log.warn("read medicalMall.func.switch.isAudit failed", e);
+            return false;
+        }
+    }
+
+    private Set<String> resolveProductExemptAuditFields() {
+        Set<String> exempt = new HashSet<>(DEFAULT_PRODUCT_EXEMPT_AUDIT_FIELDS);
+        try {
+            JSONObject switchConfig = configUtil.generateConfigByKey("medicalMall.func.switch");
+            if (switchConfig == null) {
+                return exempt;
+            }
+            JSONArray productColumns = switchConfig.getJSONArray("productColumns");
+            if (productColumns == null || productColumns.isEmpty()) {
+                return exempt;
+            }
+            for (int i = 0; i < productColumns.size(); i++) {
+                String column = productColumns.getString(i);
+                if (com.fs.common.utils.StringUtils.isNotEmpty(column)) {
+                    exempt.add(column.trim());
+                }
+            }
+        } catch (Exception e) {
+            log.warn("read productColumns failed, use default exempt fields", e);
+        }
+        return exempt;
+    }
+
+    private Set<String> collectChangedFields(Object oldObj, Object newObj) throws IllegalAccessException {
+        Set<String> changed = new HashSet<>();
+        if (oldObj == null || newObj == null || !oldObj.getClass().equals(newObj.getClass())) {
+            return changed;
+        }
+        for (Field field : oldObj.getClass().getDeclaredFields()) {
+            if (PRODUCT_AUDIT_IGNORE_FIELDS.contains(field.getName())) {
+                continue;
+            }
+            field.setAccessible(true);
+            Object oldVal = field.get(oldObj);
+            Object newVal = field.get(newObj);
+            if (!isAuditValueEqual(oldVal, newVal)) {
+                changed.add(field.getName());
+            }
+        }
+        return changed;
+    }
+
+    private boolean isAuditValueEqual(Object oldVal, Object newVal) {
+        if (Objects.equals(oldVal, newVal)) {
+            return true;
+        }
+        if (newVal == null) {
+            return true;
+        }
+        if (isBlankAuditValue(oldVal) && isBlankAuditValue(newVal)) {
+            return true;
+        }
+        if (oldVal instanceof Number || newVal instanceof Number || oldVal instanceof BigDecimal || newVal instanceof BigDecimal) {
+            BigDecimal oldBd = toAuditBigDecimal(oldVal);
+            BigDecimal newBd = toAuditBigDecimal(newVal);
+            if (oldBd != null && newBd != null) {
+                return oldBd.compareTo(newBd) == 0;
+            }
+        }
+        if (oldVal instanceof Boolean || newVal instanceof Boolean || oldVal instanceof Byte || newVal instanceof Byte) {
+            Integer oldNum = toAuditInteger(oldVal);
+            Integer newNum = toAuditInteger(newVal);
+            if (oldNum != null && newNum != null) {
+                return oldNum.equals(newNum);
+            }
+        }
+        return Objects.equals(String.valueOf(oldVal).trim(), String.valueOf(newVal).trim());
+    }
+
+    private boolean isBlankAuditValue(Object val) {
+        if (val == null) {
+            return true;
+        }
+        if (val instanceof CharSequence) {
+            return val.toString().trim().isEmpty();
+        }
+        if (val instanceof Map) {
+            return ((Map<?, ?>) val).isEmpty();
+        }
+        if (val instanceof Collection) {
+            return ((Collection<?>) val).isEmpty();
+        }
+        return false;
+    }
+
+    private BigDecimal toAuditBigDecimal(Object val) {
+        if (val == null || isBlankAuditValue(val)) {
+            return null;
+        }
+        try {
+            if (val instanceof BigDecimal) {
+                return (BigDecimal) val;
+            }
+            return new BigDecimal(String.valueOf(val).trim());
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private Integer toAuditInteger(Object val) {
+        if (val == null || isBlankAuditValue(val)) {
+            return null;
+        }
+        if (val instanceof Boolean) {
+            return Boolean.TRUE.equals(val) ? 1 : 0;
+        }
+        if (val instanceof Number) {
+            return ((Number) val).intValue();
+        }
+        try {
+            return new BigDecimal(String.valueOf(val).trim()).intValue();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private boolean hasSensitiveAttrChange(Long productId, List<FsStoreProductAttrValueScrm> newValues) {
+        if (productId == null) {
+            return false;
+        }
+        List<FsStoreProductAttrValueScrm> oldValues =
+                fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(productId);
+        List<FsStoreProductAttrValueScrm> safeNew = newValues == null ? Collections.emptyList() : newValues;
+        List<FsStoreProductAttrValueScrm> safeOld = oldValues == null ? Collections.emptyList() : oldValues;
+        if (safeNew.isEmpty() && safeOld.isEmpty()) {
+            return false;
+        }
+
+        Map<Long, FsStoreProductAttrValueScrm> oldById = new HashMap<>();
+        Map<String, FsStoreProductAttrValueScrm> oldBySku = new HashMap<>();
+        for (FsStoreProductAttrValueScrm oldVal : safeOld) {
+            if (oldVal == null) {
+                continue;
+            }
+            if (oldVal.getId() != null) {
+                oldById.put(oldVal.getId(), oldVal);
+            }
+            if (com.fs.common.utils.StringUtils.isNotEmpty(oldVal.getSku())) {
+                oldBySku.put(oldVal.getSku(), oldVal);
+            }
+        }
+
+        Set<Long> matchedOldIds = new HashSet<>();
+        boolean newHasAnyId = false;
+        for (FsStoreProductAttrValueScrm newVal : safeNew) {
+            if (newVal == null) {
+                continue;
+            }
+            if (newVal.getId() != null) {
+                newHasAnyId = true;
+            }
+            FsStoreProductAttrValueScrm oldVal = null;
+            if (newVal.getId() != null) {
+                oldVal = oldById.get(newVal.getId());
+            }
+            if (oldVal == null && com.fs.common.utils.StringUtils.isNotEmpty(newVal.getSku())) {
+                oldVal = oldBySku.get(newVal.getSku());
+            }
+            if (oldVal == null) {
+                if (newVal.getId() == null && com.fs.common.utils.StringUtils.isEmpty(newVal.getSku())) {
+                    continue;
+                }
+                log.info("product re-audit by new attr, productId={}, sku={}", productId, newVal.getSku());
+                return true;
+            }
+            if (oldVal.getId() != null) {
+                matchedOldIds.add(oldVal.getId());
+            }
+            try {
+                for (String fieldName : collectChangedFields(oldVal, newVal)) {
+                    if (PRODUCT_AUDIT_IGNORE_FIELDS.contains(fieldName)
+                            || DEFAULT_ATTR_EXEMPT_AUDIT_FIELDS.contains(fieldName)) {
+                        continue;
+                    }
+                    log.info("product re-audit by attr field, productId={}, attrId={}, field={}", productId, oldVal.getId(), fieldName);
+                    return true;
+                }
+            } catch (Exception e) {
+                log.error("attr audit diff failed, productId={}", productId, e);
+                return false;
+            }
+        }
+
+        if (newHasAnyId) {
+            for (FsStoreProductAttrValueScrm oldVal : safeOld) {
+                if (oldVal != null && oldVal.getId() != null && !matchedOldIds.contains(oldVal.getId())) {
+                    log.info("product re-audit by deleted attr, productId={}, attrId={}", productId, oldVal.getId());
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * 批量删除商品
      *
@@ -546,6 +825,9 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         }
 
         //基础信息
+        if (param.getValues() == null || param.getValues().isEmpty()) {
+            return R.error("请至少添加一个商品规格");
+        }
         ProductAttrCountDto countDto = computedProductCount(param.getValues());
         FsStoreProductScrm product = new FsStoreProductScrm();
         BeanUtils.copyProperties(param, product);
@@ -555,15 +837,14 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         populateDateRanges(product, param);
         setDefaultBarcodeIfAvailable(product, param);
         product.setBusinessLink(param.getBusinessLink());
-        product.setCreateTime(new Date());
 
-        //判断是否是纯正堂非药品商品不审核
+        // 判断是否是纯正堂非药品商品不审核
         if(CZT.equals(cloudHostProper.getCompanyName()) || param.getIsDrug() != 1){
             product.setIsAudit("1");
         }
 
-        //校验是否药品网络销售禁止清单
-        if(product.getIsDrug().equals("1")){//药品进入校验
+        // 校验是否药品网络销售禁止清单
+        if("1".equals(product.getIsDrug())){//药品进入校验
             String checkStr = checkProductInfo(product);
             if(checkStr != null){
                 return R.error(checkStr);
@@ -572,39 +853,31 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
 
         if(param.getProductId() != null && param.getProductId() > 0){
             FsStoreProductScrm oldFsStoreProduct = fsStoreProductMapper.selectFsStoreProductById(product.getProductId());
-            Boolean isAudit = configUtil.generateConfigByKey("medicalMall.func.switch").getBoolean("isAudit");
-            try {
-                if (isAudit != null && isAudit && param.getIsDrug() != 1) {
-                    if (oldFsStoreProduct.getIsAudit() != null && "1".equals(oldFsStoreProduct.getIsAudit())) {
-                        Map<String, Object> diff = getDiff(oldFsStoreProduct, product);
-                        Set<String> diff_columns = diff.keySet();
-                        JSONArray productColumns = configUtil.generateConfigByKey("medicalMall.func.switch").getJSONArray("productColumns");
-                        if(com.fs.common.utils.StringUtils.isNotEmpty(productColumns)){
-                            //判断diff_columns是否在productColumns中,不是则将isAudit设置为0
-                            for (String column : diff_columns) {
-                                if (!productColumns.contains(column)) {
-                                    product.setIsAudit("0");
-                                    break;
-                                }
-                            }
-                        }else{
-                            product.setIsAudit("0");
-                        }
-                    }
-                }
-            } catch (IllegalAccessException e) {
-                log.error("获取diff出错", e);
+            if (oldFsStoreProduct == null) {
+                return R.error("商品不存在");
             }
-            //只要是编辑,都待审核,通过审核sql 去处理audit和show字段。
-            if(oldFsStoreProduct.getIsAudit() != null){
+            // 编辑:保留原创建时间
+            product.setCreateTime(oldFsStoreProduct.getCreateTime());
+            product.setUpdateTime(DateUtils.getNowDate());
+            // 先继承旧审核状态,再按规则决定是否进入审核中
+            if (oldFsStoreProduct.getIsAudit() != null) {
+                product.setIsAudit(oldFsStoreProduct.getIsAudit());
+            }
+            boolean offlineToOnline = !Integer.valueOf(1).equals(oldFsStoreProduct.getIsShow())
+                    && Integer.valueOf(1).equals(product.getIsShow());
+            if (offlineToOnline) {
+                // 下架改上架:强制进入审核中,不依赖旧审核状态与字段差异
+                log.info("product re-audit by offlineToOnline, productId={}, oldIsShow={}, newIsShow={}",
+                        product.getProductId(), oldFsStoreProduct.getIsShow(), product.getIsShow());
+                product.setIsAudit("0");
+            } else if (needReAuditOnEdit(oldFsStoreProduct, product, param)) {
                 product.setIsAudit("0");
             }
             fsStoreProductMapper.updateFsStoreProductNullable(product);
-//            else外面也会调用这个方法,为啥会在if中调用执行handleProductAttributes
-//            handleProductAttributes(param, product, storeId);
         } else{
-            //新添加的商品默认都应该不是上架状态,流程都是需要先审核。即使你新增时候选择了上架状态,也应该是上架并待审核才对。
-            if(product.getIsShow() == 1){
+            // 新增:上架需待审核;下架不强制进审
+            product.setCreateTime(new Date());
+            if (Integer.valueOf(1).equals(product.getIsShow())) {
                 product.setIsAudit("0");
             }
             fsStoreProductMapper.insertFsStoreProduct(product);
@@ -1334,9 +1607,8 @@ private void addProductAttr(Long productId, List<ProductArrtDTO> items, List<FsS
     @Override
     @Transactional
     public void batchAudit(ProductAuditDTO auditDTO) {
-        //isAudit = 1 通过,如果通过了就需要将商品同步到产品库去,做一个id绑定。
-        //如果审核通过就是上架,如果审核不通过就是处于下架状态,为了清晰这里可以添加枚举(0未审核1审核通过2审核退回),
-        // 正常情况是退回后可以申请重新上架的如果不通过2,那么show就变回0(直接在sql中去做的),按照原有逻辑去做
+        // isAudit = 1 通过;通过后 is_show=1。若审核前商品状态已是上架,则清空 offline_remark
+        // isAudit != 1 退回;is_show=0,保留下架备注
         fsStoreProductMapper.batchAudit(auditDTO);
         storeAuditLogUtil.addBatchAuditList(auditDTO.getProductIds(),auditDTO.getReason(),auditDTO.getAttachImage());
         //审核通过进入、平台商品库
@@ -1511,6 +1783,77 @@ private void addProductAttr(Long productId, List<ProductArrtDTO> items, List<FsS
         return fsStoreProductMapper.getProductNoticeInfo(storeId);
     }
 
+    /**
+     * 资质过期自动下架。
+     * 规则:上架中的药品,营业执照/生产许可证/注册证任一到期且非长期有效,则下架并写入下架备注。
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int offlineExpiredQualificationProducts() {
+        List<FsStoreProductScrm> expiredProducts = fsStoreProductMapper.selectExpiredQualificationProducts();
+        if (expiredProducts == null || expiredProducts.isEmpty()) {
+            log.info("资质过期自动下架:无待处理商品");
+            return 0;
+        }
+        int successCount = 0;
+        LocalDate today = LocalDate.now();
+        for (FsStoreProductScrm product : expiredProducts) {
+            if (product == null || product.getProductId() == null) {
+                continue;
+            }
+            String offlineRemark = buildExpiredQualificationRemark(product, today);
+            if (com.fs.common.utils.StringUtils.isEmpty(offlineRemark)) {
+                log.warn("资质过期自动下架跳过:未拼出有效备注, productId={}", product.getProductId());
+                continue;
+            }
+            int rows = fsStoreProductMapper.offlineProductByQualification(product.getProductId(), offlineRemark);
+            if (rows > 0) {
+                successCount++;
+                log.info("资质过期自动下架成功, productId={}, productName={}, remark={}",
+                        product.getProductId(), product.getProductName(), offlineRemark);
+            }
+        }
+        log.info("资质过期自动下架完成, 待处理={}, 成功={}", expiredProducts.size(), successCount);
+        return successCount;
+    }
+
+    /**
+     * 组装资质过期下架备注(仅包含实际已过期且非长期有效的证件)
+     */
+    private String buildExpiredQualificationRemark(FsStoreProductScrm product, LocalDate today) {
+        List<String> reasons = new ArrayList<>();
+        if (isQualificationExpired(product.getBusinessEnd(), product.getIsBusinessPermanent(), today)) {
+            reasons.add("生产企业营业执照已过期");
+        }
+        if (isQualificationExpired(product.getLicenseEnd(), product.getIsLicensePermanent(), today)) {
+            reasons.add("生产企业的生产许可证/备案凭证已过期");
+        }
+        if (isQualificationExpired(product.getCertificateEnd(), product.getIsCertificatePermanent(), today)) {
+            reasons.add("商品注册证/备案凭证已过期");
+        }
+        if (reasons.isEmpty()) {
+            return null;
+        }
+        return "资质过期自动下架(" + String.join("、", reasons) + ")";
+    }
+
+    /**
+     * 证件是否已过期:到期日早于当天,且未标记长期有效
+     */
+    private boolean isQualificationExpired(LocalDate endDate, Byte permanentFlag, LocalDate today) {
+        if (endDate == null || today == null) {
+            return false;
+        }
+        if (isQualificationPermanent(permanentFlag)) {
+            return false;
+        }
+        return endDate.isBefore(today);
+    }
+
+    private boolean isQualificationPermanent(Byte permanentFlag) {
+        return permanentFlag != null && permanentFlag.byteValue() == 1;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public R copyProduct(FsStoreProductScrm product) {

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListVO.java

@@ -167,4 +167,9 @@ public class FsStoreProductListVO  implements Serializable
      * 存储条件
      */
     private String storageConditions;
+
+    /**
+     * 下架备注
+     */
+    private String offlineRemark;
 }

+ 17 - 0
fs-service/src/main/resources/mapper/company/CompanyMenuMapper.xml

@@ -158,6 +158,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		order by m.parent_id, m.order_num
 	</select>
 
+    <!-- 按用户绑定的平台角色(is_admin=1)查询菜单(含按钮) -->
+    <select id="selectMenuListByUserId" resultMap="CompanyMenuResult">
+        select distinct m.menu_id, m.menu_name, m.parent_id, m.order_num, m.path, m.component, m.is_frame, m.is_cache,
+               m.menu_type, m.visible, m.status, ifnull(m.perms,'') as perms, m.icon, m.create_by, m.create_time,
+               m.update_by, m.update_time, m.remark
+        from company_menu m
+             inner join company_role_menu rm on m.menu_id = rm.menu_id
+             inner join company_user_role ur on rm.role_id = ur.role_id
+             inner join company_role r on r.role_id = ur.role_id
+        where ur.user_id = #{userId}
+          and r.is_admin = '1'
+          and ifnull(m.status, '0') = '0'
+          and ifnull(r.status, '0') = '0'
+          and ifnull(r.del_flag, '0') = '0'
+        order by m.parent_id, m.order_num
+    </select>
+
     <select id="selectMenuListByRoleId" resultType="Integer">
         select m.menu_id
         from company_menu m

+ 13 - 1
fs-service/src/main/resources/mapper/company/CompanyRoleMapper.xml

@@ -15,6 +15,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="deptCheckStrictly"    column="dept_check_strictly"    />
         <result property="status"    column="status"    />
         <result property="delFlag"    column="del_flag"    />
+        <result property="isAdmin"    column="is_admin"    />
         <result property="createBy"    column="create_by"    />
         <result property="createTime"    column="create_time"    />
         <result property="updateBy"    column="update_by"    />
@@ -23,7 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectCompanyRoleVo">
-        select role_id, company_id, role_name, role_key, role_sort, data_scope, menu_check_strictly, dept_check_strictly, status, del_flag, create_by, create_time, update_by, update_time, remark from company_role
+        select role_id, company_id, role_name, role_key, role_sort, data_scope, menu_check_strictly, dept_check_strictly, status, del_flag, is_admin, create_by, create_time, update_by, update_time, remark from company_role
     </sql>
 
     <select id="selectCompanyRoleList" parameterType="CompanyRole" resultMap="CompanyRoleResult">
@@ -37,6 +38,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="menuCheckStrictly != null "> and menu_check_strictly = #{menuCheckStrictly}</if>
             <if test="deptCheckStrictly != null "> and dept_check_strictly = #{deptCheckStrictly}</if>
             <if test="status != null  and status != ''"> and status = #{status}</if>
+            <if test="isAdmin != null  and isAdmin != ''"> and is_admin = #{isAdmin}</if>
         </where>
     </select>
 
@@ -57,6 +59,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="deptCheckStrictly != null">dept_check_strictly,</if>
             <if test="status != null and status != ''">status,</if>
             <if test="delFlag != null">del_flag,</if>
+            <if test="isAdmin != null">is_admin,</if>
             <if test="createBy != null">create_by,</if>
             <if test="createTime != null">create_time,</if>
             <if test="updateBy != null">update_by,</if>
@@ -73,6 +76,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="deptCheckStrictly != null">#{deptCheckStrictly},</if>
             <if test="status != null and status != ''">#{status},</if>
             <if test="delFlag != null">#{delFlag},</if>
+            <if test="isAdmin != null">#{isAdmin},</if>
             <if test="createBy != null">#{createBy},</if>
             <if test="createTime != null">#{createTime},</if>
             <if test="updateBy != null">#{updateBy},</if>
@@ -85,6 +89,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         update company_role
         <trim prefix="SET" suffixOverrides=",">
             <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyId == null and isAdmin != null and isAdmin == '1'.toString()">company_id = null,</if>
             <if test="roleName != null and roleName != ''">role_name = #{roleName},</if>
             <if test="roleKey != null and roleKey != ''">role_key = #{roleKey},</if>
             <if test="roleSort != null">role_sort = #{roleSort},</if>
@@ -93,6 +98,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="deptCheckStrictly != null">dept_check_strictly = #{deptCheckStrictly},</if>
             <if test="status != null and status != ''">status = #{status},</if>
             <if test="delFlag != null">del_flag = #{delFlag},</if>
+            <if test="isAdmin != null">is_admin = #{isAdmin},</if>
             <if test="createBy != null">create_by = #{createBy},</if>
             <if test="createTime != null">create_time = #{createTime},</if>
             <if test="updateBy != null">update_by = #{updateBy},</if>
@@ -144,6 +150,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyId != null">
                 AND r.company_id = #{companyId}
             </if>
+            <if test="companyId == null">
+                AND r.company_id is null
+            </if>
         </where>
         limit 1
     </select>
@@ -157,6 +166,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyId != null">
                 AND r.company_id = #{companyId}
             </if>
+            <if test="companyId == null">
+                AND r.company_id is null
+            </if>
         </where>
         limit 1
     </select>

+ 3 - 1
fs-service/src/main/resources/mapper/company/CompanyUserRoleMapper.xml

@@ -15,7 +15,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectCompanyUserRoleList" parameterType="CompanyUserRole" resultMap="CompanyUserRoleResult">
         <include refid="selectCompanyUserRoleVo"/>
-        <where>  
+        <where>
+            <if test="userId != null">and user_id = #{userId}</if>
+            <if test="roleId != null">and role_id = #{roleId}</if>
         </where>
     </select>
     

+ 3 - 0
fs-service/src/main/resources/mapper/his/FsUserBindWxMapper.xml

@@ -312,6 +312,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             external_contact_id = #{externalContactId},
             user_company_user_id = #{userCompanyUserId},
             type = #{type}
+            <if test="bindUserId != null">
+            , fs_user_id = #{bindUserId}
+            </if>
         WHERE id = #{id}
     </update>
 

+ 2 - 1
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -48,13 +48,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="courseMaOpenId"    column="course_ma_open_id"    />
         <result property="qwExtId"    column="qw_ext_id"    />
         <result property="qwUserId"    column="qw_user_id"    />
+        <result property="phone"    column="phone"    />
         <result property="appId"    column="app_id"    />
         <result property="bizType"    column="biz_type"    />
         <result property="historyApp"    column="history_app"    />
     </resultMap>
 
     <sql id="selectFsUserVo">
-        select user_id,qw_ext_id,sex,is_buy,course_ma_open_id,is_push,is_add_qw,source,history_app,login_device,is_individuation_push,store_open_id,password,jpush_id, is_vip,vip_start_date,vip_end_date,vip_level,vip_status,nick_name,integral_status, avatar, phone, integral,sign_num, status, tui_user_id, tui_time, tui_user_count, ma_open_id, mp_open_id, union_id, is_del, user_code, remark, create_time, update_time, last_ip, balance,is_weixin_auth,parent_id,qw_user_id,company_id,company_user_id,app_id,biz_type from fs_user
+        select user_id,qw_ext_id,sex,is_buy,course_ma_open_id,is_push,is_add_qw,phone,source,history_app,login_device,is_individuation_push,store_open_id,password,jpush_id, is_vip,vip_start_date,vip_end_date,vip_level,vip_status,nick_name,integral_status, avatar, phone, integral,sign_num, status, tui_user_id, tui_time, tui_user_count, ma_open_id, mp_open_id, union_id, is_del, user_code, remark, create_time, update_time, last_ip, balance,is_weixin_auth,parent_id,qw_user_id,company_id,company_user_id,app_id,biz_type from fs_user
     </sql>
 
     <select id="selectFsUserList" parameterType="FsUser" resultMap="FsUserResult">

+ 45 - 4
fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml

@@ -99,6 +99,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="productStructure" column="product_structure"/>
         <result property="storageConditions" column="storage_conditions"/>
         <result property="specification" column="specification"/>
+        <result property="offlineRemark" column="offline_remark"/>
     </resultMap>
 
     <sql id="selectFsStoreProductVo">
@@ -116,7 +117,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                voucher, voucher_start, voucher_end,
                gmp_auth, gmp_auth_start, gmp_auth_end,business_link,medical_device_code,
                is_business_permanent,is_license_permanent,is_certificate_permanent,is_gmp_auth_permanent,platform_product_id,
-               medical_reg_cert_no, registrant_info,prod_license_no,prod_tech_req_no,product_structure,storage_conditions,specification
+               medical_reg_cert_no, registrant_info,prod_license_no,prod_tech_req_no,product_structure,storage_conditions,specification,offline_remark
                from fs_store_product_scrm
     </sql>
 
@@ -668,10 +669,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             is_audit = #{isAudit},
             <choose>
                 <when test="isAudit == 1">
-                    is_show = 1
+                    <!-- 审核通过:原商品状态为上架时清空下架备注;并置为上架 -->
+                    offline_remark = CASE WHEN is_show = 1 THEN NULL ELSE offline_remark END,
+                    is_show = 1,
+                    update_time = NOW()
                 </when>
                 <otherwise>
-                    is_show = 0
+                    is_show = 0,
+                    update_time = NOW()
                 </otherwise>
             </choose>
         </set>
@@ -977,6 +982,40 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             and is_show = 1 and is_audit = 1 and is_drug = 1
         <if test="storeId != null">AND store_id = #{storeId}</if>
     </select>
+
+    <!-- 查询资质已过期且仍上架的药品:非长期有效,且对应到期日早于当天 -->
+    <select id="selectExpiredQualificationProducts" resultMap="FsStoreProductResult">
+        SELECT product_id, product_name, common_name, store_id, is_show, is_drug, is_del,
+               business_end, license_end, certificate_end,
+               is_business_permanent, is_license_permanent, is_certificate_permanent
+        FROM fs_store_product_scrm
+        WHERE IFNULL(is_del, 0) = 0
+          AND is_show = 1
+          AND is_drug = 1
+          AND (
+                (business_end IS NOT NULL
+                    AND business_end &lt; CURDATE()
+                    AND IFNULL(is_business_permanent, 0) != 1)
+             OR (license_end IS NOT NULL
+                    AND license_end &lt; CURDATE()
+                    AND IFNULL(is_license_permanent, 0) != 1)
+             OR (certificate_end IS NOT NULL
+                    AND certificate_end &lt; CURDATE()
+                    AND IFNULL(is_certificate_permanent, 0) != 1)
+          )
+    </select>
+
+    <!-- 资质过期自动下架:写回下架状态与下架备注 -->
+    <update id="offlineProductByQualification">
+        UPDATE fs_store_product_scrm
+        SET is_show = 0,
+            offline_remark = #{offlineRemark},
+            update_time = NOW()
+        WHERE product_id = #{productId}
+          AND is_show = 1
+          AND IFNULL(is_del, 0) = 0
+    </update>
+
     <select id="getDrugExportList" resultType="com.fs.hisStore.vo.FsDrugExportVO">
         SELECT
             fss.full_name fullName,
@@ -1037,7 +1076,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        p.is_display,p.tui_cate_id,p.company_ids,p.is_drug,p.drug_image,p.drug_reg_cert_no,p.common_name,p.dosage_form,
        p.unit_price,p.batch_number,p.mah,p.mah_address,p.manufacturer,p.manufacturer_address,p.indications,p.ingredient,p.dosage,
        p.adverse_reactions,p.contraindications,p.precautions,p.is_audit,p.store_id,
-       p.is_business_permanent,p.is_license_permanent,p.is_certificate_permanent,p.is_gmp_auth_permanent
+       p.is_business_permanent,p.is_license_permanent,p.is_certificate_permanent,p.is_gmp_auth_permanent,
+       p.offline_remark
     </sql>
 
 
@@ -1046,6 +1086,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <include refid="baseSql"/>,
                 case
                     when p.is_show = 1 and p.is_audit = 1 then 1
+                    when p.is_show = 0 and p.is_audit = 1 then 0
                     when p.is_show = 0 and p.is_audit = 0 then 0
                     when p.is_show = 1 and p.is_audit = 0 then 3
                     when p.is_show = 0 and p.is_audit = 2 then 4

+ 33 - 3
fs-service/src/main/resources/mapper/system/SysUserMapper.xml

@@ -23,6 +23,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<result property="updateBy"     column="update_by"    />
 		<result property="updateTime"   column="update_time"  />
 		<result property="remark"       column="remark"       />
+		<result property="cityIds"      column="city_ids"     />
+		<result property="province"     column="province"     />
+		<result property="city"         column="city"         />
+		<result property="district"     column="district"     />
 		<association property="dept"    column="dept_id" javaType="SysDept" resultMap="deptResult" />
 		<collection  property="roles"   javaType="java.util.List"        resultMap="RoleResult" />
 		<collection property="roleName" ofType="java.lang.String" javaType="java.util.ArrayList">
@@ -48,6 +52,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<result property="updateBy"     column="update_by"    />
 		<result property="updateTime"   column="update_time"  />
 		<result property="remark"       column="remark"       />
+		<result property="cityIds"      column="city_ids"     />
+		<result property="province"     column="province"     />
+		<result property="city"         column="city"         />
+		<result property="district"     column="district"     />
 		<association property="dept"    column="dept_id" javaType="SysDept" resultMap="deptResult" />
 		<collection  property="roles"   javaType="java.util.List"        resultMap="RoleResult" />
 	</resultMap>
@@ -71,7 +79,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 	</resultMap>
 	
 	<sql id="selectUserVo">
-        select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, 
+        select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,
+        u.city_ids, u.province, u.city, u.district,
         d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,
         r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
         from sys_user u
@@ -99,6 +108,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<if test="phonenumber != null and phonenumber != ''">
 			AND u.phonenumber like concat('%', #{phonenumber}, '%')
 		</if>
+		<if test="cityIds != null and cityIds != ''">
+			AND u.city_ids = #{cityIds}
+		</if>
 		<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
 			AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
 		</if>
@@ -133,6 +145,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<if test="user.phonenumber != null and user.phonenumber != ''">
 			AND u.phonenumber like concat('%', #{user.phonenumber}, '%')
 		</if>
+		<if test="user.cityIds != null and user.cityIds != ''">
+			AND u.city_ids = #{user.cityIds}
+		</if>
 		<if test="user.params.beginTime != null and user.params.beginTime != ''"><!-- 开始时间检索 -->
 			AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{user.params.beginTime},'%y%m%d')
 		</if>
@@ -146,7 +161,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 	<select id="selectUserListByIds" parameterType="java.util.List" resultMap="SysUserResults">
 		select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber,
 		u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by,
-		u.create_time, u.remark, d.dept_name, d.leader, r.role_name
+		u.create_time, u.remark, u.city_ids, u.province, u.city, u.district, d.dept_name, d.leader, r.role_name
 		from sys_user u
 		left join sys_dept d on u.dept_id = d.dept_id
 		left join sys_user_role sur on u.user_id = sur.user_id
@@ -158,7 +173,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		order by u.user_id
 	</select>
     <select id="selectUserList" parameterType="SysUser" resultMap="SysUserResults">
-		select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader,r.role_name from sys_user u
+		select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, u.city_ids, u.province, u.city, u.district, d.dept_name, d.leader,r.role_name from sys_user u
 		left join sys_dept d on u.dept_id = d.dept_id
 		left join sys_user_role sur on u.user_id = sur.user_id
 		left join sys_role r on r.role_id = sur.role_id
@@ -175,6 +190,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<if test="phonenumber != null and phonenumber != ''">
 			AND u.phonenumber like concat('%', #{phonenumber}, '%')
 		</if>
+		<if test="cityIds != null and cityIds != ''">
+			AND u.city_ids = #{cityIds}
+		</if>
 		<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
 			AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
 		</if>
@@ -259,6 +277,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			<if test="status != null and status != ''">status,</if>
  			<if test="createBy != null and createBy != ''">create_by,</if>
  			<if test="remark != null and remark != ''">remark,</if>
+ 			<if test="cityIds != null and cityIds != ''">city_ids,</if>
+ 			<if test="province != null and province != ''">province,</if>
+ 			<if test="city != null and city != ''">city,</if>
+ 			<if test="district != null and district != ''">district,</if>
  			create_time
  		)values(
  			<if test="userId != null and userId != ''">#{userId},</if>
@@ -273,6 +295,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			<if test="status != null and status != ''">#{status},</if>
  			<if test="createBy != null and createBy != ''">#{createBy},</if>
  			<if test="remark != null and remark != ''">#{remark},</if>
+ 			<if test="cityIds != null and cityIds != ''">#{cityIds},</if>
+ 			<if test="province != null and province != ''">#{province},</if>
+ 			<if test="city != null and city != ''">#{city},</if>
+ 			<if test="district != null and district != ''">#{district},</if>
  			sysdate()
  		)
 	</insert>
@@ -293,6 +319,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			<if test="loginDate != null">login_date = #{loginDate},</if>
  			<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
  			<if test="remark != null">remark = #{remark},</if>
+ 			<if test="cityIds != null">city_ids = #{cityIds},</if>
+ 			<if test="province != null">province = #{province},</if>
+ 			<if test="city != null">city = #{city},</if>
+ 			<if test="district != null">district = #{district},</if>
  			update_time = sysdate()
  		</set>
  		where user_id = #{userId}

+ 2 - 1
fs-store/src/main/resources/application.yml

@@ -6,4 +6,5 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: dev-yjb
+    #    active: dev-yjb
+    active: druid-yjb-test #医健宝测试库

+ 18 - 16
fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java

@@ -297,27 +297,30 @@ public class AppLoginController extends AppBaseController{
             Integer sex = (Integer) userInfo.get("sex");
             String avatar = userInfo.get("headimgurl").toString();
 
-            FsUser user = findUserByPhoneAppId(param.getPhone(),appid);
-            if (user!=null && user.getUnionId() == null){
-                FsUser userByUnionId = userMapper.selectFsUserByUnionid(unionid);
+            List<FsUser> userList = findUsersByPhone(param.getPhone());
+            if(!userList.isEmpty()){
+                FsUser user = userList.stream().findFirst().orElse(null);
+                if (user!=null && user.getUnionId() == null){
+                    FsUser userByUnionId = userMapper.selectFsUserByUnionid(unionid);
 //                if (userByUnionId!=null){
 //                    return R.error("该微信已绑定其他用户,请勿重复绑定!");
 //                }
-                user.setSource(param.getSource() != null ? param.getSource() : null );
-                user.setLoginDevice(param.getLoginDevice() != null ? param.getLoginDevice() : null);
-                user.setNickName(nickname);
-                user.setAvatar(avatar);
-                if(StringUtils.isNotBlank(openid)){
-                    user.setMaOpenId(openid);
-                }
-                user.setSex(sex);
-                user.setUnionId(unionid);
-                if (userService.updateFsUser(user)>0){
-                    return generateTokenAndReturn(user);
-                }
+                    user.setSource(param.getSource() != null ? param.getSource() : null );
+                    user.setLoginDevice(param.getLoginDevice() != null ? param.getLoginDevice() : null);
+                    user.setNickName(nickname);
+                    user.setAvatar(avatar);
+                    if(StringUtils.isNotBlank(openid)){
+                        user.setMaOpenId(openid);
+                    }
+                    user.setSex(sex);
+                    user.setUnionId(unionid);
+                    if (userService.updateFsUser(user)>0){
+                        return generateTokenAndReturn(user);
+                    }
 //                else {
 //                    return R.error("绑定微信失败");
 //                }
+                }
             }
             return R.error("数据参数异常!");
         }catch (Exception e){
@@ -812,7 +815,6 @@ public class AppLoginController extends AppBaseController{
         String encryptPhone = encryptPhone(phone);
         String appid = openProperties.getAppId();
         Integer bizType = fsCoursePlaySourceConfigService.resolveBizTypeByAppId(appid);
-        logger.info("打印数据--------------》1:{}",bizType);
         if(bizType == null){
             if(appid.equals("wx7796a33a71912e32")){
                 bizType = 2;

+ 149 - 10
fs-user-app/src/main/java/com/fs/app/controller/UserController.java

@@ -1,25 +1,30 @@
 package com.fs.app.controller;
 
 
+import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.lang.Validator;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.app.annotation.Login;
-import com.fs.app.param.FriendsSearchParam;
 import com.fs.app.param.FsDoctorRegisterParam;
+import com.fs.app.param.FsUserBindPhoneParam;
 import com.fs.app.param.FsUserEditParam;
+import com.fs.common.VerifyCodeUtil;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.exception.CustomException;
+import com.fs.common.service.ISmsService;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
 import com.fs.common.utils.sign.Md5Utils;
 import com.fs.common.utils.txocr.IdCardVerificationResponse;
 import com.fs.common.utils.txocr.TxOcrClient;
-import com.fs.company.domain.CompanyUser;
+import com.fs.core.config.WxOpenProperties;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.his.domain.FsDoctor;
-import com.fs.his.domain.FsPackage;
 import com.fs.his.domain.FsUser;
 import com.fs.his.dto.FindUsersByDTO;
+import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.FindUserByParam;
 import com.fs.his.param.FsAddUserAndSaleFriendParam;
 import com.fs.his.param.FsUserCouponUParam;
@@ -29,8 +34,6 @@ import com.fs.his.service.IFsPackageService;
 import com.fs.his.service.IFsUserCouponService;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.utils.PhoneUtil;
-import com.fs.his.utils.qrcode.QRCodeUtils;
-import com.fs.his.vo.FsDoctorListUVO;
 import com.fs.his.vo.FsUserCouponCountUVO;
 import com.fs.his.vo.FsUserCouponListUVO;
 import com.fs.his.vo.UserVo;
@@ -41,9 +44,7 @@ import com.fs.system.oss.CloudStorageService;
 import com.fs.system.oss.OSSFactory;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
-import com.google.zxing.WriterException;
 import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.lang.Assert;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -51,7 +52,6 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.ibatis.annotations.Param;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.core.parameters.P;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -68,10 +68,9 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
-
-import static com.fs.common.utils.SecurityUtils.getUserId;
 import static com.fs.his.utils.PhoneUtil.decryptPhoneMk;
 import static com.fs.his.utils.PhoneUtil.encryptPhone;
+import static com.fs.his.utils.PhoneUtil.encryptPhoneOldKey;
 
 
 @Api("个人中心")
@@ -98,6 +97,16 @@ public class UserController extends  AppBaseController {
     private IFsStoreScrmService fsStoreService;
     @Autowired
     private IFsUserService fsUserService;
+
+    @Autowired
+    private WxOpenProperties openProperties;
+
+    @Autowired
+    private ISmsService smsService;
+
+    @Autowired
+    private FsUserMapper userMapper;
+
     @Login
     @ApiOperation("获取用户信息")
     @PostMapping("/friendsSearch")
@@ -437,4 +446,134 @@ public class UserController extends  AppBaseController {
         }
         return userService.addUserAndSaleFriend(user,param.getCompayUserId(),param.getExternalUserId());
     }
+
+
+    @Login
+    @ApiOperation("绑定手机号验证码发送")
+    @PostMapping("/sendCode")
+    public R sendCode(@RequestBody Map<String, String> body){
+        String phone = body.get("phone");
+        String type = body.get("type");
+        if(org.apache.commons.lang3.StringUtils.isEmpty(type)){
+            return R.error("请选择验证码类型");
+        }
+
+        // IP 维度限频(同一IP一小时最多10条)
+        String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+        String ipKey = "sms:ip:" + ip;
+        Integer ipCount = redisCache.getCacheObject(ipKey);
+        if (ipCount != null && ipCount >= 10) {
+            return R.error("当前IP短信发送次数已达上限,请稍后再试");
+        }
+
+        // 验证码 key(3分钟有效)
+        String smsCodeKey = "sms:registration:code:" + phone;
+        // 冷却 key(60秒内不能重复发送)
+        String smsCooldownKey = "sms:registration:cooldown:" + phone;
+
+        // 判断是否在 60 秒冷却期
+        if (redisCache.getCacheObject(smsCooldownKey) != null) {
+            return R.error("验证码已发送,请稍后再试");
+        }
+
+        // 生成新验证码
+        String smsCode = VerifyCodeUtil.generateCode();
+
+        // 发送短信
+        smsService.sendCaptcha(phone, smsCode, "验证码",type);
+
+        // 缓存验证码(3分钟有效)
+        redisCache.setCacheObject(smsCodeKey, smsCode, 180, TimeUnit.SECONDS);
+        // 设置冷却时间(60秒内不能再发)
+        redisCache.setCacheObject(smsCooldownKey, "1", 60, TimeUnit.SECONDS);
+        // IP计数(1小时过期)
+        redisCache.setCacheObject(ipKey, (ipCount == null ? 0 : ipCount) + 1, 1, TimeUnit.HOURS);
+
+        return R.ok("验证码已发送");
+    }
+
+    /**
+     * 绑定手机号:校验 sendCode 验证码后,处理 biz_type=1 下同号账号,再更新当前用户手机号
+     */
+    @Login
+    @ApiOperation("绑定手机号")
+    @PostMapping("/bindPhone")
+    public R bindPhone(@Validated @RequestBody FsUserBindPhoneParam param) {
+        String phone = param.getPhone() == null ? null : param.getPhone().trim();
+        String code = param.getCode() == null ? null : param.getCode().trim();
+        if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {
+            return R.error("手机号和验证码不能为空");
+        }
+
+        // 1. token 取当前用户
+        String userIdStr = getUserId();
+        if (StringUtils.isEmpty(userIdStr)) {
+            return R.error("未登录或登录已失效");
+        }
+        Long userId = Long.parseLong(userIdStr);
+        FsUser currentUser = userService.selectFsUserById(userId);
+        if (currentUser == null) {
+            return R.error("当前用户不存在");
+        }
+
+        // 2. 校验 sendCode 写入的验证码
+        String smsCodeKey = "sms:registration:code:" + phone;
+        String redisCode = redisCache.getCacheObject(smsCodeKey);
+        if (StringUtils.isEmpty(redisCode)) {
+            return R.error("验证码已失效,请重新获取");
+        }
+        if (!redisCode.equals(code)) {
+            return R.error("验证码错误");
+        }
+
+        List<FsUser> phoneUsers = findUsersByPhone(phone);
+        if (phoneUsers != null && !phoneUsers.isEmpty()) {
+            // 当前用户已是该手机号且自身有 unionId:无需重复绑定
+            boolean selfAlreadyBound = phoneUsers.stream().anyMatch(u ->
+                    StringUtils.isNotEmpty(u.getUnionId()));
+            if (selfAlreadyBound && StringUtils.isNotEmpty(currentUser.getPhone())) {
+                redisCache.deleteObject(smsCodeKey);
+                return R.ok("已绑定手机号,无需重复绑定");
+            }
+
+            // 筛选 unionId 为空的脏数据,真实删除(排除当前用户)
+            for (FsUser u : phoneUsers) {
+                if (StringUtils.isEmpty(u.getUnionId())) {
+                    userService.physicalDeleteFsUserByUserId(u.getUserId());
+                    log.info("绑定手机号清理无unionId脏数据, deleteUserId={}, phone={}", u.getUserId(), phone);
+                }
+            }
+        }
+
+        // 4. 写入当前用户手机号
+        FsUser update = new FsUser();
+        update.setUserId(userId);
+        update.setPhone(phone);
+        if (userService.updateFsUser(update) <= 0) {
+            return R.error("绑定手机号失败");
+        }
+        redisCache.deleteObject(smsCodeKey);
+        return R.ok("绑定手机号成功");
+    }
+
+    private List<FsUser> findUsersByPhone(String phone) {
+        String appid = openProperties.getAppId();
+        Integer bizType = 1;
+        if(appid.equals("wx7796a33a71912e32")){
+            bizType =2;
+        }
+        // 先根据加密手机号查询用户
+        String jiami = (encryptPhone(phone));
+        List<FsUser> fsUsers = userMapper.selectFsUsersByPhoneLimitOne(jiami,bizType);
+        if (CollectionUtil.isEmpty(fsUsers)) {
+            fsUsers = userMapper.selectFsUsersByPhoneLimitOne(encryptPhoneOldKey(phone),bizType);
+        }
+        // 如果没有找到用户,再根据手机号查询
+        if (CollectionUtil.isEmpty(fsUsers)) {
+            fsUsers = userMapper.selectFsUsersByPhoneLimitOne(phone,bizType);
+
+        }
+        return fsUsers;
+    }
+
 }

+ 106 - 0
fs-user-app/src/main/java/com/fs/app/controller/WxH5MpController.java

@@ -4,11 +4,13 @@ import cn.binarywang.wx.miniapp.api.WxMaService;
 import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
 import cn.hutool.core.date.DateTime;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.alibaba.fastjson.JSON;
 import com.fs.app.annotation.UserOperationLog;
 import com.fs.app.param.FsUserLoginByCourseMpParam;
 import com.fs.app.param.FsUserLoginByMpParam;
 import com.fs.app.utils.JwtUtils;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.ResponseResult;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyTrafficRecord;
@@ -19,6 +21,8 @@ import com.fs.company.service.ICompanyUserService;
 import com.fs.core.config.WxMaConfiguration;
 import com.fs.course.domain.FsUserCompanyUser;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.param.newfs.FsUserCourseBeMemberParam;
+import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserWx;
@@ -83,6 +87,8 @@ public class WxH5MpController {
 
     @Autowired
     private CompanyTrafficRecordMapper companyTrafficRecordMapper;
+    @Autowired
+    private IFsCourseLinkService courseLinkService;
     @Value("${cloud_host.company_name}")
     private String signProjectName;
 
@@ -149,6 +155,106 @@ public class WxH5MpController {
         }
     }
 
+    @ApiOperation("H5好友注册:公众号授权登录并按uuid绑定销售")
+    @PostMapping("/loginByMpH5")
+    @UserOperationLog(operationType = FsUserOperationEnum.H5LOGIN)
+    public R loginByMpH5(@RequestBody FsUserLoginByMpParam param) {
+        log.info("=====================进入H5授权登录绑定, 入参: {}", JSON.toJSONString(param));
+        if (param == null) {
+            return R.error("参数不能为空");
+        }
+        if (StringUtils.isBlank(param.getCode())) {
+            return R.error("code不存在");
+        }
+        // 兼容 uuid / uuId
+        String uuid = StringUtils.isNotBlank(param.getUuId()) ? param.getUuId().trim() : null;
+        if (StringUtils.isBlank(uuid)) {
+            return R.error("uuid不能为空");
+        }
+
+        // 与 beMemberH5 / getBindLink 一致:从 Redis APPBIND:uuid 还原绑定信息
+        Map<String, Object> bindInfo;
+        try {
+            bindInfo = courseLinkService.getBindInfoByUuid(uuid);
+        } catch (Exception e) {
+            log.error("H5登录绑定读取uuid失败 uuid={}", uuid, e);
+            return R.error(StringUtils.defaultIfBlank(e.getMessage(), "绑定链接已失效或不存在"));
+        }
+        Long companyId = bindInfo.get("companyId") == null ? null : Long.valueOf(bindInfo.get("companyId").toString());
+        Long companyUserId = bindInfo.get("companyUserId") == null ? null : Long.valueOf(bindInfo.get("companyUserId").toString());
+        Long projectId = bindInfo.get("projectId") == null ? null : Long.valueOf(bindInfo.get("projectId").toString());
+        if (companyId == null || companyUserId == null || projectId == null) {
+            return R.error("绑定链接数据不完整");
+        }
+        param.setCompanyId(companyId);
+        param.setCompanyUserId(companyUserId);
+        param.setProjectId(projectId);
+        param.setUuId(uuid);
+
+        // 公司 / 销售状态校验(与 loginByMp 一致)
+        Company company = companyService.selectCompanyById(companyId);
+        if (company == null || company.getStatus() == 0) {
+            return R.error("注册失败团队已停用,或不存在!");
+        }
+        CompanyUser companyUser = companyUserService.selectCompanyUserById(companyUserId);
+        if (companyUser == null || "1".equals(companyUser.getStatus())) {
+            return R.error("注册失败客服已停用,或不存在!");
+        }
+        if ("医健宝".equals(signProjectName)) {
+            CompanyTrafficRecord companyTrafficRecord = companyTrafficRecordMapper.selectOne(
+                    new QueryWrapper<CompanyTrafficRecord>().eq("company_id", companyId));
+            if (companyTrafficRecord == null || companyTrafficRecord.getBalance() <= 0) {
+                return R.error(500, "暂时无法观看,请联系管理员(流量)");
+            }
+        }
+
+        try {
+            WxOAuth2AccessToken wxMpOAuth2AccessToken = wxMpService.getOAuth2Service().getAccessToken(param.getCode());
+            WxOAuth2UserInfo wxMpUser = wxMpService.getOAuth2Service().getUserInfo(wxMpOAuth2AccessToken, null);
+            if (StringUtils.isEmpty(wxMpUser.getUnionId())) {
+                return R.error("未绑定开放平台");
+            }
+
+            FsUser user = processUserInfo(wxMpUser, company, companyUser, param);
+            if (user == null || user.getUserId() == null) {
+                return R.error("用户登录失败");
+            }
+
+            // 登录成功后直接走 beMemberH5 同等绑定逻辑
+            FsUserCourseBeMemberParam beMemberParam = new FsUserCourseBeMemberParam();
+            beMemberParam.setUserId(user.getUserId());
+            beMemberParam.setCompanyId(companyId);
+            beMemberParam.setCompanyUserId(companyUserId);
+            beMemberParam.setProjectId(projectId);
+            ResponseResult<Boolean> bindResult = userService.becomeMember(beMemberParam);
+            log.info("H5登录后绑定结果 userId={}, bindCode={}, bindMsg={}",
+                    user.getUserId(), bindResult.getCode(), bindResult.getMsg());
+
+            String token = jwtUtils.generateToken(user.getUserId());
+            redisCache.setCacheObject("token:" + user.getUserId(), token, 604800, TimeUnit.SECONDS);
+
+            Map<String, Object> result = new HashMap<>(8);
+            result.put("token", token);
+            result.put("user", user);
+            result.put("uuid", uuid);
+            result.put("companyId", companyId);
+            result.put("companyUserId", companyUserId);
+            result.put("projectId", projectId);
+            result.put("bindCode", bindResult.getCode());
+            result.put("bindMsg", bindResult.getMsg());
+            // 200 成功;402 已注册待审核,也视为绑定流程完成
+            result.put("bindSuccess", Integer.valueOf(200).equals(bindResult.getCode())
+                    || Integer.valueOf(402).equals(bindResult.getCode()));
+            return R.ok(result);
+        } catch (WxErrorException e) {
+            this.logger.error(e.getMessage(), e);
+            return R.error("授权失败," + e.getMessage());
+        } catch (Exception e) {
+            log.error("H5授权登录绑定异常", e);
+            return R.error("登录绑定失败:" + e.getMessage());
+        }
+    }
+
     @ApiOperation("炮灰小程序-公众号登录转小程序")
     @PostMapping("/courseLoginByMp")
     @UserOperationLog(operationType = FsUserOperationEnum.H5LOGIN)

+ 45 - 0
fs-user-app/src/main/java/com/fs/app/controller/store/UserScrmController.java

@@ -5,8 +5,10 @@ import cn.hutool.core.img.ImgUtil;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.extra.qrcode.QrCodeUtil;
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.app.annotation.Login;
 import com.fs.app.controller.AppBaseController;
+import com.fs.app.param.FsUserCourseBeMemberH5Param;
 import com.fs.common.OrderUtils;
 import com.fs.common.config.FSConfig;
 import com.fs.common.core.domain.R;
@@ -368,4 +370,47 @@ public class UserScrmController extends AppBaseController {
         log.debug("会员关联绑定销售 param:{}", JSON.toJSONString(param));
         return fsUserService.becomeMember(param);
     }
+
+    @ApiOperation("会员关联绑定销售(H5,通过uuid从Redis还原绑定信息)")
+    @PostMapping("/beMemberH5")
+    public ResponseResult<Boolean> beMemberH5(@RequestBody FsUserCourseBeMemberH5Param h5Param) {
+        log.debug("会员关联绑定销售H5 param:{}", JSON.toJSONString(h5Param));
+        if (h5Param == null) {
+            return ResponseResult.fail(500, "参数不能为空");
+        }
+        if (StringUtils.isBlank(h5Param.getUuid())) {
+            return ResponseResult.fail(500, "uuid不能为空");
+        }
+        if (h5Param.getUserId() == null) {
+            return ResponseResult.fail(500, "用户id不能为空");
+        }
+
+        // 与 getBindLink 存入规则一致:APPBIND:雪花ID
+        String uuid = h5Param.getUuid().trim();
+        String redisKey = uuid.startsWith("APPBIND:") ? uuid : ("APPBIND:" + uuid);
+        String cacheData = redisCache.getCacheObject(redisKey);
+        if (StringUtils.isBlank(cacheData)) {
+            return ResponseResult.fail(500, "绑定链接已失效或不存在");
+        }
+
+        JSONObject cacheJson = JSON.parseObject(cacheData);
+        if (cacheJson == null) {
+            return ResponseResult.fail(500, "绑定链接数据异常");
+        }
+        Long companyId = cacheJson.getLong("companyId");
+        Long companyUserId = cacheJson.getLong("companyUserId");
+        Long projectId = cacheJson.getLong("projectId");
+        if (companyId == null || companyUserId == null || projectId == null) {
+            return ResponseResult.fail(500, "绑定链接数据不完整");
+        }
+
+        FsUserCourseBeMemberParam param = new FsUserCourseBeMemberParam();
+        param.setUserId(h5Param.getUserId());
+//        param.setTagIds(h5Param.getTagIds());
+        param.setCompanyId(companyId);
+        param.setCompanyUserId(companyUserId);
+        param.setProjectId(projectId);
+        log.debug("会员关联绑定销售H5 组装后 param:{}", JSON.toJSONString(param));
+        return fsUserService.becomeMember(param);
+    }
 }

+ 28 - 0
fs-user-app/src/main/java/com/fs/app/param/FsUserBindPhoneParam.java

@@ -0,0 +1,28 @@
+package com.fs.app.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+import java.io.Serializable;
+
+/**
+ * 用户绑定手机号参数
+ */
+@Data
+@ApiModel("用户绑定手机号参数")
+public class FsUserBindPhoneParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank(message = "手机号不能为空")
+    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
+    @ApiModelProperty(value = "手机号", required = true)
+    private String phone;
+
+    @NotBlank(message = "验证码不能为空")
+    @ApiModelProperty(value = "短信验证码", required = true)
+    private String code;
+}

+ 25 - 0
fs-user-app/src/main/java/com/fs/app/param/FsUserCourseBeMemberH5Param.java

@@ -0,0 +1,25 @@
+package com.fs.app.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * H5
+ */
+@Data
+@ApiModel("H5")
+public class FsUserCourseBeMemberH5Param implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "key", required = true)
+    private String uuid;
+
+    @ApiModelProperty(value = "用户id", required = true)
+    private Long userId;
+
+    private String[] tagIds;
+}

+ 5 - 0
fs-user-app/src/main/java/com/fs/app/param/FsUserLoginByMpParam.java

@@ -31,4 +31,9 @@ public class FsUserLoginByMpParam implements Serializable {
      * 用户id
      */
     private Long userId;
+
+    /**
+     * 兼容旧字段名
+     */
+    private String uuId;
 }

+ 1 - 1
fs-user-app/src/main/java/com/fs/app/utils/UniVerifyUtil.java

@@ -30,7 +30,7 @@ public class UniVerifyUtil {
 
     private static final Logger log = LoggerFactory.getLogger(UniVerifyUtil.class);
 
-    @Value("${univerify.cloud-function-url:https://fc-mp-788b1d8f-c300-465c-9e38-c986b162ae93.next.bspapp.com/getLoginMobile}")
+    @Value("${univerify.cloud-function-url}")
     private String cloudFunctionUrl;
 
     /**

+ 2 - 2
fs-user-app/src/main/resources/application.yml

@@ -13,5 +13,5 @@ spring:
 #    active: druid-sxjz
 #    active: druid-qdtst
 #    active: druid-yzt
-#    active: dev-yjb
-    active: druid-yjb-test #医健宝测试库
+    active: dev-yjb
+#    active: druid-yjb-test #医健宝测试库

+ 4 - 0
sql/company_role_add_is_admin.sql

@@ -0,0 +1,4 @@
+-- company_role add is_admin
+-- 0: no (default); 1: yes (created by admin platform)
+ALTER TABLE company_role
+    ADD COLUMN is_admin CHAR(1) NOT NULL DEFAULT '0' COMMENT '是否管理员设置(0否 1是,平台端新增为1)' AFTER del_flag;

+ 4 - 0
sql/company_role_company_id_nullable.sql

@@ -0,0 +1,4 @@
+-- 平台销售角色不绑定公司:确保 company_id 允许为空
+-- 若原字段已是可空可跳过
+ALTER TABLE company_role
+    MODIFY COLUMN company_id BIGINT NULL DEFAULT NULL COMMENT '销售公司ID(平台管理员角色为空)';

+ 4 - 0
sql/fs_store_product_scrm_add_offline_remark.sql

@@ -0,0 +1,4 @@
+-- ��Ʒ�������¼ܱ�ע�ֶΣ����ʹ����Զ��¼ܵȳ���д��ԭ��
+-- ִ��ǰ��ȷ�ϱ� fs_store_product_scrm ���ڣ������� offline_remark �ֶ�
+ALTER TABLE fs_store_product_scrm
+    ADD COLUMN offline_remark VARCHAR(500) NULL DEFAULT NULL COMMENT '�¼ܱ�ע';