DESKTOP-3Q5PM1M\896986 4 napja
szülő
commit
972774de52
22 módosított fájl, 601 hozzáadás és 62 törlés
  1. 13 0
      complaint-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
  2. 6 0
      complaint-system/src/main/resources/mapper/system/SysUserMapper.xml
  3. 9 13
      complaint-workorder/src/main/java/com/complaint/workorder/controller/api/ApiComplaintController.java
  4. 59 0
      complaint-workorder/src/main/java/com/complaint/workorder/controller/web/ComplaintController.java
  5. 19 0
      complaint-workorder/src/main/java/com/complaint/workorder/controller/web/ComplaintStaffController.java
  6. 5 0
      complaint-workorder/src/main/java/com/complaint/workorder/domain/StaffImportDTO.java
  7. 6 1
      complaint-workorder/src/main/java/com/complaint/workorder/mapper/ComplaintStaffMapper.java
  8. 5 0
      complaint-workorder/src/main/java/com/complaint/workorder/mapper/UnitSchoolMapper.java
  9. 6 0
      complaint-workorder/src/main/java/com/complaint/workorder/service/IComplaintStaffService.java
  10. 199 12
      complaint-workorder/src/main/java/com/complaint/workorder/service/impl/ComplaintStaffServiceImpl.java
  11. 8 0
      complaint-workorder/src/main/resources/mapper/workorder/ComplaintStaffMapper.xml
  12. 8 0
      complaint-workorder/src/main/resources/mapper/workorder/UnitSchoolMapper.xml
  13. 8 3
      h5-app/src/pages/ComplaintForm.vue
  14. 26 0
      ruoyi-ui/src/api/complaint/school.js
  15. 1 1
      ruoyi-ui/src/components/Breadcrumb/index.vue
  16. 1 1
      ruoyi-ui/src/layout/components/Navbar.vue
  17. 1 1
      ruoyi-ui/src/layout/components/TagsView/index.vue
  18. 1 1
      ruoyi-ui/src/layout/components/TopNav/index.vue
  19. 4 4
      ruoyi-ui/src/store/modules/lock.js
  20. 1 1
      ruoyi-ui/src/utils/request.js
  21. 149 6
      ruoyi-ui/src/views/complaint/school/index.vue
  22. 66 18
      ruoyi-ui/src/views/system/user/index.vue

+ 13 - 0
complaint-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java

@@ -60,6 +60,9 @@ public class SysUser extends BaseEntity
     /** 查询参数:unit_uid 非空筛选(非数据库字段,学校管理员列表用) */
     private Boolean unitUidNotNull;
 
+    /** 查询参数:所属区域代码(非数据库字段,关联unit_school表) */
+    private String ssqydm;
+
     /** 用户性别 */
     @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
     private String sex;
@@ -231,6 +234,16 @@ public class SysUser extends BaseEntity
         this.unitUidNotNull = unitUidNotNull;
     }
 
+    public String getSsqydm()
+    {
+        return ssqydm;
+    }
+
+    public void setSsqydm(String ssqydm)
+    {
+        this.ssqydm = ssqydm;
+    }
+
     public String getSex()
     {
         return sex;

+ 6 - 0
complaint-system/src/main/resources/mapper/system/SysUserMapper.xml

@@ -70,6 +70,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<if test="userName != null and userName != ''">
 			AND u.user_name like '%' || #{userName} || '%'
 		</if>
+		<if test="nickName != null and nickName != ''">
+			AND u.nick_name like '%' || #{nickName} || '%'
+		</if>
 		<if test="status != null and status != ''">
 			AND u.status = #{status}
 		</if>
@@ -88,6 +91,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<if test="unitUid != null and unitUid != ''">
 			AND u.unit_uid = #{unitUid}
 		</if>
+		<if test="ssqydm != null and ssqydm != ''">
+			AND us.ssqydm = #{ssqydm}
+		</if>
 		<if test="unitUidNotNull != null and unitUidNotNull == true">
 			AND u.unit_uid IS NOT NULL AND u.unit_uid != ''
 		</if>

+ 9 - 13
complaint-workorder/src/main/java/com/complaint/workorder/controller/api/ApiComplaintController.java

@@ -158,24 +158,20 @@ public class ApiComplaintController extends BaseController
     }
 
     /**
-     * 县区列表(SSQYDM 字典
+     * 县区列表(从字典 complaint_district 查询
      */
     @GetMapping("/school/districts")
     public AjaxResult districts()
     {
+        List<SysDictData> dictList = dictTypeService.selectDictDataByType("complaint_district");
         List<Map<String, String>> list = new ArrayList<>();
-        Map<String, String> hbw = new LinkedHashMap<>();
-        hbw.put("code", "HBW");
-        hbw.put("name", "海勃湾区");
-        list.add(hbw);
-        Map<String, String> wd = new LinkedHashMap<>();
-        wd.put("code", "WD");
-        wd.put("name", "乌达区");
-        list.add(wd);
-        Map<String, String> hn = new LinkedHashMap<>();
-        hn.put("code", "HN");
-        hn.put("name", "海南区");
-        list.add(hn);
+        for (SysDictData dict : dictList)
+        {
+            Map<String, String> item = new LinkedHashMap<>();
+            item.put("code", dict.getDictValue());
+            item.put("name", dict.getDictLabel());
+            list.add(item);
+        }
         return AjaxResult.success(list);
     }
 

+ 59 - 0
complaint-workorder/src/main/java/com/complaint/workorder/controller/web/ComplaintController.java

@@ -460,6 +460,65 @@ public class ComplaintController extends BaseController
         return AjaxResult.success(list);
     }
 
+    /**
+     * 新增学校机构
+     */
+    @PreAuthorize("@ss.hasPermi('complaint:school:add')")
+    @Log(title = "学校机构", businessType = BusinessType.INSERT)
+    @PostMapping("/school")
+    public AjaxResult addSchool(@RequestBody UnitSchool school)
+    {
+        if (StringUtils.isBlank(school.getUnitName()))
+        {
+            return AjaxResult.error("机构名称不能为空");
+        }
+        // 机构ID为空时自动生成32位UUID(和东软同步的格式一致)
+        if (StringUtils.isBlank(school.getUnitUid()))
+        {
+            school.setUnitUid(java.util.UUID.randomUUID().toString().replace("-", ""));
+        }
+        // 检查是否已存在
+        UnitSchool exist = unitSchoolMapper.selectUnitByUid(school.getUnitUid());
+        if (exist != null)
+        {
+            return AjaxResult.error("机构UID已存在");
+        }
+        school.setYxbs(StringUtils.isBlank(school.getYxbs()) ? "1" : school.getYxbs());
+        school.setCreateBy(SecurityUtils.getUsername());
+        school.setSyncTime(new java.util.Date());
+        int rows = unitSchoolMapper.insertUnitSchool(school);
+        return toAjax(rows);
+    }
+
+    /**
+     * 修改学校机构
+     */
+    @PreAuthorize("@ss.hasPermi('complaint:school:edit')")
+    @Log(title = "学校机构", businessType = BusinessType.UPDATE)
+    @PutMapping("/school")
+    public AjaxResult editSchool(@RequestBody UnitSchool school)
+    {
+        if (StringUtils.isBlank(school.getUnitUid()))
+        {
+            return AjaxResult.error("机构UID不能为空");
+        }
+        school.setUpdateBy(SecurityUtils.getUsername());
+        int rows = unitSchoolMapper.updateUnitSchool(school);
+        return toAjax(rows);
+    }
+
+    /**
+     * 删除学校机构
+     */
+    @PreAuthorize("@ss.hasPermi('complaint:school:remove')")
+    @Log(title = "学校机构", businessType = BusinessType.DELETE)
+    @DeleteMapping("/school/{unitUid}")
+    public AjaxResult delSchool(@PathVariable("unitUid") String unitUid)
+    {
+        int rows = unitSchoolMapper.deleteUnitSchool(unitUid);
+        return toAjax(rows);
+    }
+
     @PreAuthorize("@ss.hasPermi('complaint:school:sync')")
     @Log(title = "机构数据同步", businessType = BusinessType.OTHER)
     @PostMapping("/school/sync")

+ 19 - 0
complaint-workorder/src/main/java/com/complaint/workorder/controller/web/ComplaintStaffController.java

@@ -1,8 +1,10 @@
 package com.complaint.workorder.controller.web;
 
 import java.util.List;
+import java.util.ArrayList;
 import jakarta.servlet.http.HttpServletResponse;
 import com.complaint.workorder.domain.ComplaintStaff;
+import com.complaint.workorder.domain.StaffImportDTO;
 import com.complaint.workorder.service.IComplaintStaffService;
 import com.ruoyi.common.annotation.Log;
 import com.ruoyi.common.core.controller.BaseController;
@@ -148,4 +150,21 @@ public class ComplaintStaffController extends BaseController
         ExcelUtil<ComplaintStaff> util = new ExcelUtil<ComplaintStaff>(ComplaintStaff.class);
         util.importTemplateExcel(response, "教职工导入模板");
     }
+
+    /**
+     * 导出最近一次导入中学校为空的人员列表
+     */
+    @PreAuthorize("@ss.hasPermi('workorder:staff:export')")
+    @Log(title = "导出学校为空人员", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportEmptySchool")
+    public void exportEmptySchool(HttpServletResponse response)
+    {
+        List<StaffImportDTO> list = staffService.getEmptySchoolList();
+        if (list == null || list.isEmpty())
+        {
+            list = new ArrayList<>();
+        }
+        ExcelUtil<StaffImportDTO> util = new ExcelUtil<StaffImportDTO>(StaffImportDTO.class);
+        util.exportExcel(response, list, "学校为空人员列表");
+    }
 }

+ 5 - 0
complaint-workorder/src/main/java/com/complaint/workorder/domain/StaffImportDTO.java

@@ -17,10 +17,15 @@ public class StaffImportDTO
     @Excel(name = "手机号码")
     private String mobile;
 
+    @Excel(name = "性别")
+    private String sex;
+
     public String getUserName() { return userName; }
     public void setUserName(String userName) { this.userName = userName; }
     public String getUnitName() { return unitName; }
     public void setUnitName(String unitName) { this.unitName = unitName; }
     public String getMobile() { return mobile; }
     public void setMobile(String mobile) { this.mobile = mobile; }
+    public String getSex() { return sex; }
+    public void setSex(String sex) { this.sex = sex; }
 }

+ 6 - 1
complaint-workorder/src/main/java/com/complaint/workorder/mapper/ComplaintStaffMapper.java

@@ -24,10 +24,15 @@ public interface ComplaintStaffMapper
     public ComplaintStaff selectComplaintStaffByMobile(String mobile);
 
     /**
-     * 根据姓名+学校+手机号匹配教职工(导入用)
+     * 根据姓名+学校+手机号精确匹配教职工(导入用)
      */
     public ComplaintStaff matchStaff(ComplaintStaff staff);
 
+    /**
+     * 根据姓名+手机号+学校名模糊匹配教职工(导入用,学校简称匹配)
+     */
+    public ComplaintStaff matchStaffFuzzy(ComplaintStaff staff);
+
     /**
      * 新增教职工
      */

+ 5 - 0
complaint-workorder/src/main/java/com/complaint/workorder/mapper/UnitSchoolMapper.java

@@ -38,4 +38,9 @@ public interface UnitSchoolMapper
      * 全部学校列表(PC 后台)
      */
     public List<UnitSchool> selectUnitList(UnitSchool school);
+
+    /**
+     * 根据学校名称模糊查询(导入用,学校简称匹配)
+     */
+    public UnitSchool selectUnitByNameFuzzy(@Param("unitName") String unitName);
 }

+ 6 - 0
complaint-workorder/src/main/java/com/complaint/workorder/service/IComplaintStaffService.java

@@ -3,6 +3,7 @@ package com.complaint.workorder.service;
 import java.util.List;
 import org.springframework.web.multipart.MultipartFile;
 import com.complaint.workorder.domain.ComplaintStaff;
+import com.complaint.workorder.domain.StaffImportDTO;
 
 /**
  * 教职工信息Service接口
@@ -44,4 +45,9 @@ public interface IComplaintStaffService
      * 匹配条件:姓名 + 所属学校 + 手机号码
      */
     public String importStaff(MultipartFile file) throws Exception;
+
+    /**
+     * 获取最近一次导入中学校为空的人员列表(用于导出)
+     */
+    public List<StaffImportDTO> getEmptySchoolList();
 }

+ 199 - 12
complaint-workorder/src/main/java/com/complaint/workorder/service/impl/ComplaintStaffServiceImpl.java

@@ -5,8 +5,10 @@ import java.util.Date;
 import java.util.List;
 import com.alibaba.fastjson2.JSONObject;
 import com.complaint.workorder.domain.ComplaintStaff;
+import com.complaint.workorder.domain.UnitSchool;
 import com.complaint.workorder.domain.StaffImportDTO;
 import com.complaint.workorder.mapper.ComplaintStaffMapper;
+import com.complaint.workorder.mapper.UnitSchoolMapper;
 import com.complaint.workorder.service.IComplaintStaffService;
 import com.complaint.workorder.sync.ODataClient;
 import com.ruoyi.common.annotation.Excel;
@@ -33,6 +35,9 @@ public class ComplaintStaffServiceImpl implements IComplaintStaffService
 {
     private static final Logger log = LoggerFactory.getLogger(ComplaintStaffServiceImpl.class);
 
+    /** 临时存储:最近一次导入中学校为空的人员列表(用于导出) */
+    private static final List<StaffImportDTO> lastEmptySchoolList = new ArrayList<>();
+
     private static final String INTERFACE_NAME = "RYJCSJXXZL_4";
     private static final String FILTER = "ID_TYPE_NAME eq '教职工'";
     private static final String SCHOOL_ADMIN_ROLE_NAME = "学校管理员";
@@ -41,6 +46,9 @@ public class ComplaintStaffServiceImpl implements IComplaintStaffService
     @Autowired
     private ComplaintStaffMapper staffMapper;
 
+    @Autowired
+    private UnitSchoolMapper unitSchoolMapper;
+
     @Autowired
     private ODataClient oDataClient;
 
@@ -159,6 +167,9 @@ public class ComplaintStaffServiceImpl implements IComplaintStaffService
     @Override
     public String importStaff(MultipartFile file) throws Exception
     {
+        // 清空上次的学校为空人员列表
+        lastEmptySchoolList.clear();
+
         ExcelUtil<StaffImportDTO> util = new ExcelUtil<StaffImportDTO>(StaffImportDTO.class);
         List<StaffImportDTO> importList = util.importExcel(file.getInputStream());
 
@@ -178,13 +189,21 @@ public class ComplaintStaffServiceImpl implements IComplaintStaffService
         int fail = 0;
         int notFound = 0;
         int alreadyExists = 0;
+        int fuzzyMatched = 0;
         List<String> failMsgs = new ArrayList<>();
+        List<String> emptySchoolMsgs = new ArrayList<>();  // 学校为空的人员
 
         for (int i = 0; i < importList.size(); i++)
         {
             StaffImportDTO dto = importList.get(i);
             int rowNum = i + 1;
 
+            // 跳过空行
+            if (dto == null)
+            {
+                continue;
+            }
+
             if (StringUtils.isBlank(dto.getUserName()) || StringUtils.isBlank(dto.getMobile()))
             {
                 fail++;
@@ -194,6 +213,17 @@ public class ComplaintStaffServiceImpl implements IComplaintStaffService
 
             try
             {
+                // 学校为空,记录下来
+                if (StringUtils.isBlank(dto.getUnitName()))
+                {
+                    emptySchoolMsgs.add("第" + rowNum + "行:" + dto.getUserName().trim() + "(" + dto.getMobile().trim() + ")");
+                    StaffImportDTO emptyDto = new StaffImportDTO();
+                    emptyDto.setUserName(dto.getUserName().trim());
+                    emptyDto.setMobile(dto.getMobile().trim());
+                    emptyDto.setUnitName("");
+                    lastEmptySchoolList.add(emptyDto);
+                }
+
                 // 根据姓名+学校+手机号匹配教职工表
                 ComplaintStaff match = new ComplaintStaff();
                 match.setUserName(dto.getUserName().trim());
@@ -201,34 +231,74 @@ public class ComplaintStaffServiceImpl implements IComplaintStaffService
                 match.setUnitName(StringUtils.isBlank(dto.getUnitName()) ? "" : dto.getUnitName().trim());
                 ComplaintStaff staff = staffMapper.matchStaff(match);
 
+                // 精确匹配不到,且学校名不为空,尝试模糊匹配(学校简称匹配)
+                if (staff == null && StringUtils.isNotBlank(dto.getUnitName()))
+                {
+                    staff = staffMapper.matchStaffFuzzy(match);
+                    if (staff != null)
+                    {
+                        fuzzyMatched++;
+                        log.info("第{}行:模糊匹配成功(输入学校={},匹配到={})", rowNum, dto.getUnitName(), staff.getUnitName());
+                    }
+                }
+
                 if (staff == null)
                 {
                     notFound++;
-                    failMsgs.add("第" + rowNum + "行:未匹配到教职工(姓名=" + dto.getUserName() + ",手机=" + dto.getMobile() + ")");
-                    continue;
+                    log.info("第{}行:未匹配到教职工,将直接创建用户(姓名={},手机={})", rowNum, dto.getUserName(), dto.getMobile());
                 }
 
                 // 检查系统用户是否已存在
                 SysUser existUser = sysUserMapper.checkPhoneUnique(dto.getMobile().trim());
                 if (existUser != null)
                 {
-                    // 已存在,更新学工号和学校UID
-                    existUser.setStaffNo(staff.getIdNumber());
-                    existUser.setUnitUid(staff.getUnitUid());
-                    sysUserMapper.updateUser(existUser);
                     alreadyExists++;
-                    success++;
+                    // 已存在用户:比较学校名称,不一致则更新(模糊匹配)
+                    boolean updated = updateUserIfSchoolChanged(existUser, dto, rowNum);
+                    if (updated)
+                    {
+                        success++;
+                    }
                     continue;
                 }
 
                 // 创建系统用户
                 SysUser user = new SysUser();
                 user.setUserName(dto.getMobile().trim());  // 用户名用手机号
-                user.setNickName(staff.getUserName());
+                user.setNickName(staff != null ? staff.getUserName() : dto.getUserName().trim());
                 user.setPhonenumber(dto.getMobile().trim());
                 user.setPassword(SecurityUtils.encryptPassword(DEFAULT_PASSWORD));
-                user.setUnitUid(staff.getUnitUid());
-                user.setStaffNo(staff.getIdNumber());
+                if (staff != null)
+                {
+                    user.setUnitUid(staff.getUnitUid());
+                    user.setStaffNo(staff.getIdNumber());
+                }
+                else if (StringUtils.isNotBlank(dto.getUnitName()))
+                {
+                    // 教职工匹配不到,从学校表模糊查询学校UID
+                    UnitSchool school = unitSchoolMapper.selectUnitByNameFuzzy(dto.getUnitName().trim());
+                    if (school != null)
+                    {
+                        user.setUnitUid(school.getUnitUid());
+                        log.info("第{}行:从学校表匹配到学校(输入学校={},匹配到={},UID={})",
+                                rowNum, dto.getUnitName(), school.getUnitName(), school.getUnitUid());
+                    }
+                    else
+                    {
+                        log.warn("第{}行:学校表也未匹配到学校(输入学校={})", rowNum, dto.getUnitName());
+                    }
+                }
+                // 设置性别:优先使用Excel中的性别,其次用教职工的性别,最后设为未知(2)
+                String userSex = convertSex(dto.getSex());
+                if (StringUtils.isBlank(userSex) && staff != null && StringUtils.isNotBlank(staff.getUserSex()))
+                {
+                    userSex = staff.getUserSex();
+                }
+                if (StringUtils.isBlank(userSex))
+                {
+                    userSex = "2";  // 未知
+                }
+                user.setSex(userSex);
                 user.setStatus("0");  // 正常
                 user.setRoleIds(new Long[]{schoolAdminRoleId});
                 user.setCreateBy("import");
@@ -254,9 +324,26 @@ public class ComplaintStaffServiceImpl implements IComplaintStaffService
 
         StringBuilder result = new StringBuilder();
         result.append("导入完成:成功").append(success).append("条");
-        result.append("(其中已存在更新").append(alreadyExists).append("条)");
-        result.append(",未匹配").append(notFound).append("条");
+        result.append("(其中已存在跳过").append(alreadyExists).append("条");
+        result.append(",模糊匹配学校").append(fuzzyMatched).append("条)");
+        result.append(",未匹配教职工但已创建").append(notFound).append("条");
         result.append(",失败").append(fail).append("条");
+        if (!emptySchoolMsgs.isEmpty())
+        {
+            result.append("。学校为空的人员(").append(emptySchoolMsgs.size()).append("人):");
+            if (emptySchoolMsgs.size() <= 10)
+            {
+                result.append(String.join(";", emptySchoolMsgs));
+            }
+            else
+            {
+                result.append("数量过多,请查看日志");
+                for (String msg : emptySchoolMsgs)
+                {
+                    log.warn("学校为空:{}", msg);
+                }
+            }
+        }
         if (!failMsgs.isEmpty() && failMsgs.size() <= 10)
         {
             result.append("。失败详情:").append(String.join(";", failMsgs));
@@ -310,4 +397,104 @@ public class ComplaintStaffServiceImpl implements IComplaintStaffService
         staff.setDataUpdateFlog(row.getString("DATA_UPDATE_FLOG"));
         return staff;
     }
+
+    /**
+     * 导出最近一次导入中学校为空的人员列表
+     */
+    @Override
+    public List<StaffImportDTO> getEmptySchoolList()
+    {
+        return new ArrayList<>(lastEmptySchoolList);
+    }
+
+    /**
+     * 转换性别:中文转数字
+     * 男 -> 0, 女 -> 1, 未知 -> 2, 其他 -> null
+     */
+    private String convertSex(String sex)
+    {
+        if (StringUtils.isBlank(sex))
+        {
+            return null;
+        }
+        String trimmed = sex.trim();
+        if ("男".equals(trimmed) || "0".equals(trimmed))
+        {
+            return "0";
+        }
+        if ("女".equals(trimmed) || "1".equals(trimmed))
+        {
+            return "1";
+        }
+        if ("未知".equals(trimmed) || "2".equals(trimmed))
+        {
+            return "2";
+        }
+        return null;
+    }
+
+    /**
+     * 已存在用户:比较学校名称,不一致则更新(模糊匹配)
+     * @return true=更新了,false=未更新
+     */
+    private boolean updateUserIfSchoolChanged(SysUser existUser, StaffImportDTO dto, int rowNum)
+    {
+        try
+        {
+            // 导入的学校名为空,不更新
+            if (StringUtils.isBlank(dto.getUnitName()))
+            {
+                log.info("第{}行:用户已存在,导入学校为空,不更新(手机号={})", rowNum, dto.getMobile());
+                return false;
+            }
+
+            // 用导入的学校名模糊匹配教职工表,获取最新的教职工信息
+            ComplaintStaff match = new ComplaintStaff();
+            match.setUserName(dto.getUserName().trim());
+            match.setMobile(dto.getMobile().trim());
+            match.setUnitName(dto.getUnitName().trim());
+            ComplaintStaff staff = staffMapper.matchStaffFuzzy(match);
+
+            // 模糊匹配不到教职工,不更新
+            if (staff == null)
+            {
+                log.info("第{}行:用户已存在,模糊匹配不到教职工,不更新(手机号={},学校={})", rowNum, dto.getMobile(), dto.getUnitName());
+                return false;
+            }
+
+            // 比较学校UID是否一致
+            String currentUnitUid = existUser.getUnitUid();
+            String newUnitUid = staff.getUnitUid();
+            String currentStaffNo = existUser.getStaffNo();
+            String newStaffNo = staff.getIdNumber();
+
+            boolean unitChanged = !StringUtils.equals(currentUnitUid, newUnitUid);
+            boolean staffNoChanged = !StringUtils.equals(currentStaffNo, newStaffNo);
+
+            if (!unitChanged && !staffNoChanged)
+            {
+                log.info("第{}行:用户已存在,学校信息一致,不更新(手机号={})", rowNum, dto.getMobile());
+                return false;
+            }
+
+            // 学校信息不一致,更新
+            if (unitChanged)
+            {
+                existUser.setUnitUid(newUnitUid);
+            }
+            if (staffNoChanged)
+            {
+                existUser.setStaffNo(newStaffNo);
+            }
+            sysUserMapper.updateUser(existUser);
+            log.info("第{}行:用户已存在,学校信息已更新(手机号={},原学校UID={},新学校UID={},原学工号={},新学工号={})",
+                    rowNum, dto.getMobile(), currentUnitUid, newUnitUid, currentStaffNo, newStaffNo);
+            return true;
+        }
+        catch (Exception e)
+        {
+            log.error("第{}行:更新已存在用户学校信息失败(手机号={})", rowNum, dto.getMobile(), e);
+            return false;
+        }
+    }
 }

+ 8 - 0
complaint-workorder/src/main/resources/mapper/workorder/ComplaintStaffMapper.xml

@@ -73,6 +73,14 @@
         fetch first 1 rows only
     </select>
 
+    <select id="matchStaffFuzzy" parameterType="com.complaint.workorder.domain.ComplaintStaff" resultMap="ComplaintStaffResult">
+        <include refid="selectComplaintStaffVo"/>
+        where user_name = #{userName}
+          and mobile = #{mobile}
+          and (unit_name like '%' || #{unitName} || '%' or belong_school like '%' || #{unitName} || '%')
+        fetch first 1 rows only
+    </select>
+
     <select id="selectByUserUid" parameterType="String" resultMap="ComplaintStaffResult">
         <include refid="selectComplaintStaffVo"/>
         where user_uid = #{userUid}

+ 8 - 0
complaint-workorder/src/main/resources/mapper/workorder/UnitSchoolMapper.xml

@@ -88,4 +88,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		order by ssqydm, unit_name
 	</select>
 
+    <select id="selectUnitByNameFuzzy" resultType="com.complaint.workorder.domain.UnitSchool">
+        select unit_uid as unitUid, unit_name as unitName
+        from unit_school
+        where unit_name like '%' || #{unitName} || '%'
+          and yxbs = '1'
+        fetch first 1 rows only
+    </select>
+
 </mapper>

+ 8 - 3
h5-app/src/pages/ComplaintForm.vue

@@ -152,14 +152,19 @@ function onPickerConfirm(params) {
   showPicker.value = false
 }
 
+// 完整区域数据(code + name),用于学校查询时的代码映射
+const districtList = ref([])
+
 async function loadDistricts() {
   const res = await getDistricts()
-  districts.value = (res.data || []).map(d => ({ text: d.name, value: d.name }))
+  districtList.value = res.data || []
+  districts.value = districtList.value.map(d => ({ text: d.name, value: d.name }))
 }
 
 async function loadSchools() {
-  const codeMap = { '海勃湾区': 'HBW', '乌达区': 'WD', '海南区': 'HN' }
-  const res = await getSchools(codeMap[form.district] || '')
+  // 从区域列表中查找对应的区域代码
+  const district = districtList.value.find(d => d.name === form.district)
+  const res = await getSchools(district ? district.code : '')
   schoolsData.value = res.data || []
 }
 

+ 26 - 0
ruoyi-ui/src/api/complaint/school.js

@@ -34,3 +34,29 @@ export function listSyncLog(query) {
     params: query
   })
 }
+
+// 新增学校机构
+export function addSchool(data) {
+  return request({
+    url: '/complaint/school',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改学校机构
+export function updateSchool(data) {
+  return request({
+    url: '/complaint/school',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除学校机构
+export function delSchool(unitUid) {
+  return request({
+    url: '/complaint/school/' + unitUid,
+    method: 'delete'
+  })
+}

+ 1 - 1
ruoyi-ui/src/components/Breadcrumb/index.vue

@@ -34,7 +34,7 @@ function getBreadcrumb() {
   }
   // 判断是否为首页
   if (!isDashboard(matched[0])) {
-    matched = [{ path: "/dashboard", meta: { title: "首页" } }].concat(matched)
+    matched = [{ path: "/data/dashboard", meta: { title: "首页" } }].concat(matched)
   }
   levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
 }

+ 1 - 1
ruoyi-ui/src/layout/components/Navbar.vue

@@ -116,7 +116,7 @@ function logout() {
     type: 'warning'
   }).then(() => {
     userStore.logOut().then(() => {
-      location.href = '/dashboard'
+      location.href = '/data/dashboard'
     })
   }).catch(() => { })
 }

+ 1 - 1
ruoyi-ui/src/layout/components/TagsView/index.vue

@@ -156,7 +156,7 @@ function isAffix(tag) {
 function isFirstView() {
   try {
     const tag = selectedTag.value && selectedTag.value.fullPath ? selectedTag.value : selectedDropdownTag.value
-    return tag.fullPath === '/dashboard' || tag.fullPath === visitedViews.value[1].fullPath
+    return tag.fullPath === '/data/dashboard' || tag.fullPath === visitedViews.value[1].fullPath
   } catch (err) {
     return false
   }

+ 1 - 1
ruoyi-ui/src/layout/components/TopNav/index.vue

@@ -44,7 +44,7 @@ const visibleNumber = ref(null)
 // 当前激活菜单的 index
 const currentIndex = ref(null)
 // 隐藏侧边栏路由
-const hideList = ['/dashboard', '/user/profile']
+const hideList = ['/data/dashboard', '/user/profile']
 
 const appStore = useAppStore()
 const settingsStore = useSettingsStore()

+ 4 - 4
ruoyi-ui/src/store/modules/lock.js

@@ -4,12 +4,12 @@ const LOCK_PATH_KEY = 'screen-lock-path'
 export const useLockStore = defineStore('lock', {
   state: () => ({
     isLock: JSON.parse(localStorage.getItem(LOCK_KEY) || 'false'),
-    lockPath: localStorage.getItem(LOCK_PATH_KEY) || '/dashboard'
+    lockPath: localStorage.getItem(LOCK_PATH_KEY) || '/data/dashboard'
   }),
   actions: {
     // 锁定屏幕,同时记录当前路径
     lockScreen(currentPath) {
-      this.lockPath = currentPath || '/dashboard'
+      this.lockPath = currentPath || '/data/dashboard'
       localStorage.setItem(LOCK_PATH_KEY, this.lockPath)
       this.isLock = true
       localStorage.setItem(LOCK_KEY, 'true')
@@ -18,8 +18,8 @@ export const useLockStore = defineStore('lock', {
     unlockScreen() {
       this.isLock = false
       localStorage.setItem(LOCK_KEY, 'false')
-      this.lockPath = '/dashboard'
-      localStorage.setItem(LOCK_PATH_KEY, '/dashboard')
+      this.lockPath = '/data/dashboard'
+      localStorage.setItem(LOCK_PATH_KEY, '/data/dashboard')
     }
   }
 })

+ 1 - 1
ruoyi-ui/src/utils/request.js

@@ -88,7 +88,7 @@ service.interceptors.response.use(res => {
         ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
           isRelogin.show = false
           useUserStore().logOut().then(() => {
-            location.href = '/dashboard'
+            location.href = '/data/dashboard'
           })
       }).catch(() => {
         isRelogin.show = false

+ 149 - 6
ruoyi-ui/src/views/complaint/school/index.vue

@@ -29,6 +29,9 @@
             <el-button type="success" plain icon="Refresh" :loading="syncLoading" @click="handleDailySync" 
               v-hasPermi="['complaint:school:sync']">每日同步</el-button>
           </el-col>
+          <el-col :span="2">
+            <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['complaint:school:add']">新增</el-button>
+          </el-col>
           <el-col :span="14.5">
             <span style="color: #909399; line-height: 32px; font-size: 13px">
               ✅ 推荐方式:点击“每日同步”触发增量数据拉取(安全、高效)<br>
@@ -61,6 +64,12 @@
               <span>{{ parseTime(scope.row.syncTime) }}</span>
             </template>
           </el-table-column>
+          <el-table-column label="操作" align="center" width="150" fixed="right">
+            <template #default="scope">
+              <el-button link type="primary" icon="Edit" @click="handleEdit(scope.row)" v-hasPermi="['complaint:school:edit']">修改</el-button>
+              <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['complaint:school:remove']">删除</el-button>
+            </template>
+          </el-table-column>
         </el-table>
 
         <pagination
@@ -70,6 +79,39 @@
           v-model:limit="schoolQuery.pageSize"
           @pagination="getSchoolList"
         />
+
+        <!-- 新增/修改弹窗 -->
+        <el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
+          <el-form ref="schoolFormRef" :model="schoolForm" :rules="schoolRules" label-width="100px">
+            <el-form-item label="机构UID" prop="unitUid">
+              <el-input v-model="schoolForm.unitUid" placeholder="不填则自动生成" :disabled="isEdit" />
+            </el-form-item>
+            <el-form-item label="机构名称" prop="unitName">
+              <el-input v-model="schoolForm.unitName" placeholder="请输入机构名称" />
+            </el-form-item>
+            <el-form-item label="所属区域" prop="ssqydm">
+              <el-select v-model="schoolForm.ssqydm" placeholder="请选择区域" clearable style="width: 100%" @change="handleDistrictChange">
+                <el-option v-for="d in districtOptions" :key="d.value" :label="d.label" :value="d.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="单位类型" prop="dwlxmc">
+              <el-input v-model="schoolForm.dwlxmc" placeholder="请输入单位类型" />
+            </el-form-item>
+            <el-form-item label="学段" prop="xddm">
+              <el-input v-model="schoolForm.xddm" placeholder="请输入学段" />
+            </el-form-item>
+            <el-form-item label="有效标识" prop="yxbs">
+              <el-select v-model="schoolForm.yxbs" placeholder="请选择" style="width: 100%">
+                <el-option label="有效" value="1" />
+                <el-option label="无效" value="0" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+          <template #footer>
+            <el-button @click="dialogVisible = false">取 消</el-button>
+            <el-button type="primary" @click="submitSchoolForm">确 定</el-button>
+          </template>
+        </el-dialog>
       </el-tab-pane>
 
       <!-- 同步日志 -->
@@ -131,7 +173,8 @@
 </template>
 
 <script setup name="ComplaintSchool">
-import { listSchool, syncSchoolDaily, listSyncLog } from "@/api/complaint/school"
+import { listSchool, syncSchoolDaily, listSyncLog, addSchool, updateSchool, delSchool } from "@/api/complaint/school"
+import { getDicts } from "@/api/system/dict/data"
 
 const { proxy } = getCurrentInstance()
 
@@ -144,11 +187,35 @@ const logList = ref([])
 const logLoading = ref(false)
 const logTotal = ref(0)
 
-const districtOptions = [
-  { value: 'HBW', label: '海勃湾区' },
-  { value: 'WD', label: '乌达区' },
-  { value: 'HN', label: '海南区' }
-]
+const districtOptions = ref([])
+
+// 弹窗相关
+const dialogVisible = ref(false)
+const dialogTitle = ref("")
+const isEdit = ref(false)
+const schoolFormRef = ref(null)
+const schoolForm = reactive({
+  unitUid: "",
+  unitName: "",
+  ssqydm: "",
+  ssqymc: "",
+  dwlxmc: "",
+  xddm: "",
+  yxbs: "1"
+})
+const schoolRules = {
+  unitName: [{ required: true, message: "机构名称不能为空", trigger: "blur" }]
+}
+
+// 从字典加载区域列表
+function loadDistrictOptions() {
+  getDicts("complaint_district").then(response => {
+    districtOptions.value = response.data.map(item => ({
+      value: item.dictValue,
+      label: item.dictLabel
+    }))
+  })
+}
 
 const data = reactive({
   schoolQuery: {
@@ -168,6 +235,9 @@ const data = reactive({
 
 const { schoolQuery, logQuery } = toRefs(data)
 
+// 页面加载时加载字典数据
+loadDistrictOptions()
+
 /** 查询学校机构列表 */
 function getSchoolList() {
   schoolLoading.value = true
@@ -232,6 +302,79 @@ watch(activeTab, (tab) => {
   }
 })
 
+/** 新增按钮 */
+function handleAdd() {
+  isEdit.value = false
+  dialogTitle.value = "新增学校机构"
+  Object.assign(schoolForm, {
+    unitUid: "",
+    unitName: "",
+    ssqydm: "",
+    ssqymc: "",
+    dwlxmc: "",
+    xddm: "",
+    yxbs: "1"
+  })
+  dialogVisible.value = true
+}
+
+/** 修改按钮 */
+function handleEdit(row) {
+  isEdit.value = true
+  dialogTitle.value = "修改学校机构"
+  Object.assign(schoolForm, {
+    unitUid: row.unitUid,
+    unitName: row.unitName,
+    ssqydm: row.ssqydm,
+    ssqymc: row.ssqymc,
+    dwlxmc: row.dwlxmc,
+    xddm: row.xddm,
+    yxbs: row.yxbs || "1"
+  })
+  dialogVisible.value = true
+}
+
+/** 删除按钮 */
+function handleDelete(row) {
+  proxy.$modal.confirm('确认要删除学校机构"' + row.unitName + '"吗?').then(function () {
+    return delSchool(row.unitUid)
+  }).then(() => {
+    getSchoolList()
+    proxy.$modal.msgSuccess("删除成功")
+  }).catch(() => {})
+}
+
+/** 区域选择变化:同时设置区域代码和区域名称 */
+function handleDistrictChange(value) {
+  if (value) {
+    const district = districtOptions.value.find(d => d.value === value)
+    schoolForm.ssqymc = district ? district.label : ""
+  } else {
+    schoolForm.ssqymc = ""
+  }
+}
+
+/** 提交表单 */
+function submitSchoolForm() {
+  schoolFormRef.value.validate(valid => {
+    if (valid) {
+      if (isEdit.value) {
+        updateSchool(schoolForm).then(response => {
+          proxy.$modal.msgSuccess("修改成功")
+          dialogVisible.value = false
+          getSchoolList()
+        })
+      } else {
+        addSchool(schoolForm).then(response => {
+          proxy.$modal.msgSuccess("新增成功")
+          dialogVisible.value = false
+          getSchoolList()
+        })
+      }
+    }
+  })
+}
+
 getSchoolList()
 </script>
 <style scoped>

+ 66 - 18
ruoyi-ui/src/views/system/user/index.vue

@@ -4,8 +4,8 @@
     <div class="tree-sidebar-content">
       <div class="content-inner">
         <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
-          <el-form-item label="用户名称" prop="userName">
-            <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
+          <el-form-item label="昵称" prop="nickName">
+            <el-input v-model="queryParams.nickName" placeholder="请输入昵称" clearable style="width: 200px" @keyup.enter="handleQuery" />
           </el-form-item>
           <el-form-item label="手机号码" prop="phonenumber">
             <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable style="width: 180px" @keyup.enter="handleQuery" />
@@ -51,12 +51,19 @@
         <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
           <el-table-column type="selection" width="50" align="center" />
           <el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns.userId.visible" />
-          <el-table-column label="用户名" align="center" key="userName" v-if="columns.userName.visible" :show-overflow-tooltip="true">
+          <el-table-column label="用户名" align="center" key="userName" v-if="columns.userName.visible" :show-overflow-tooltip="true">
             <template #default="scope">
               <a class="link-type" style="cursor:pointer" @click="handleViewData(scope.row)">{{ scope.row.userName }}</a>
             </template>
          </el-table-column>
-          <el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns.nickName.visible" :show-overflow-tooltip="true" />
+          <el-table-column label="昵称" align="center" key="nickName" prop="nickName" v-if="columns.nickName.visible" :show-overflow-tooltip="true" />
+          <el-table-column label="性别" align="center" key="sex" prop="sex" v-if="columns.sex.visible" width="80">
+            <template #default="scope">
+              <el-tag v-if="scope.row.sex === '0'" type="primary" size="small">男</el-tag>
+              <el-tag v-else-if="scope.row.sex === '1'" type="danger" size="small">女</el-tag>
+              <el-tag v-else type="info" size="small">未知</el-tag>
+            </template>
+          </el-table-column>
           <el-table-column label="所属学校" align="center" key="schoolName" v-if="columns.schoolName.visible" :show-overflow-tooltip="true">
             <template #default="scope">
               <span>{{ getSchoolName(scope.row.unitUid) }}</span>
@@ -210,6 +217,7 @@ import UserViewDrawer from "./view"
 import { usePasswordRule } from "@/utils/passwordRule"
 import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser } from "@/api/system/user"
 import { listSchool } from "@/api/complaint/school"
+import { getDicts } from "@/api/system/dict/data"
 
 const router = useRouter()
 const { proxy } = getCurrentInstance()
@@ -235,8 +243,9 @@ const identityOptions = ['校长', '副校长', '主任', '副主任', '经办
 // 列显隐信息
 const columns = ref({
   userId: { label: '用户编号', visible: true },
-  userName: { label: '用户名称', visible: true },
-  nickName: { label: '用户昵称', visible: true },
+  userName: { label: '用户名', visible: true },
+  nickName: { label: '昵称', visible: true },
+  sex: { label: '性别', visible: true },
   schoolName: { label: '所属学校', visible: true },
   identity: { label: '职位', visible: true },
   phonenumber: { label: '手机号码', visible: true },
@@ -249,7 +258,7 @@ const data = reactive({
   queryParams: {
     pageNum: 1,
     pageSize: 10,
-    userName: undefined,
+    nickName: undefined,
     phonenumber: undefined,
     status: undefined,
     unitUid: undefined
@@ -281,22 +290,61 @@ function getList() {
   })
 }
 
-/** 查询学校列表(构造成左侧树) */
+/** 区域列表(从字典获取) */
+const districtList = ref([])
+
+/** 查询学校列表(构造成左侧树,按区域分组) */
 function getSchoolTree() {
-  listSchool({ pageNum: 1, pageSize: 999 }).then(response => {
-    schoolOptions.value = response.rows || []
-    // 构造成一层树:根节点"全部学校" + 学校列表
-    schoolTree.value = [{
-      id: '',
-      label: '全部学校',
-      children: schoolOptions.value.map(s => ({ id: s.unitUid, label: s.unitName }))
-    }]
+  // 先获取区域字典
+  getDicts("complaint_district").then(dictRes => {
+    districtList.value = dictRes.data || []
+    // 再获取学校列表
+    listSchool({ pageNum: 1, pageSize: 999 }).then(response => {
+      schoolOptions.value = response.rows || []
+      // 构造成两层树:根节点"全部学校" → 区域节点 → 学校节点
+      const districtNodes = districtList.value.map(d => {
+        const schoolsInDistrict = schoolOptions.value.filter(s => s.ssqydm === d.dictValue)
+        return {
+          id: 'district_' + d.dictValue,
+          label: d.dictLabel,
+          districtCode: d.dictValue,
+          children: schoolsInDistrict.map(s => ({ id: s.unitUid, label: s.unitName }))
+        }
+      })
+      // 没有区域的学校放到"其他"节点
+      const schoolsNoDistrict = schoolOptions.value.filter(s => !s.ssqydm || !districtList.value.find(d => d.dictValue === s.ssqydm))
+      if (schoolsNoDistrict.length > 0) {
+        districtNodes.push({
+          id: 'district_other',
+          label: '其他',
+          districtCode: '',
+          children: schoolsNoDistrict.map(s => ({ id: s.unitUid, label: s.unitName }))
+        })
+      }
+      schoolTree.value = [{
+        id: '',
+        label: '全部学校',
+        children: districtNodes
+      }]
+    })
   })
 }
 
-/** 节点单击事件(按学校筛选) */
+/** 节点单击事件(按区域或学校筛选) */
 function handleNodeClick(data) {
-  queryParams.value.unitUid = data.id === '' ? undefined : data.id
+  if (data.id === '') {
+    // 根节点:全部学校
+    queryParams.value.unitUid = undefined
+    queryParams.value.ssqydm = undefined
+  } else if (data.id.startsWith('district_')) {
+    // 区域节点:按区域筛选
+    queryParams.value.unitUid = undefined
+    queryParams.value.ssqydm = data.districtCode || undefined
+  } else {
+    // 学校节点:按学校筛选
+    queryParams.value.unitUid = data.id
+    queryParams.value.ssqydm = undefined
+  }
   handleQuery()
 }