Преглед на файлове

添加返回运营城市

jinshihui преди 2 седмици
родител
ревизия
cd6c8826cb

+ 3 - 3
nightFragrance-admin/src/main/java/com/ylx/web/controller/massage/MaTechnicianController.java

@@ -374,14 +374,14 @@ public class MaTechnicianController extends BaseController {
     }
 
     /**
-     * 技师状态切换
+     * 商户状态切换
      *
      * @param userId 商户ID
-     * @param forceConfirm 是否强制切换
+     * @param forceConfirm 是否强制切换 true:强制切换 false:不强制切换
      * @return Result<?>
      */
     @GetMapping("/switchToOffline")
-    @ApiOperation("技师状态切换")
+    @ApiOperation("商户状态切换")
     public Result switchToOffline(@RequestParam Long userId, @RequestParam Boolean forceConfirm) {
         try {
             return maTechnicianService.switchToOffline(userId, forceConfirm);

+ 3 - 1
nightFragrance-massage/src/main/java/com/ylx/massage/domain/MaProject.java

@@ -2,6 +2,7 @@ package com.ylx.massage.domain;
 
 import java.math.BigDecimal;
 
+import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModel;
@@ -19,7 +20,7 @@ import java.util.Date;
  */
 @ApiModel(value = "MaProject", description = "服务项目")
 @Data
-@TableName("Ma_Project")
+@TableName("ma_project")
 public class MaProject {
     private static final long serialVersionUID = 1L;
 
@@ -169,6 +170,7 @@ public class MaProject {
      */
     @ApiModelProperty("是否删除(0否1是)")
     @Excel(name = "是否删除(0否1是)")
+    @TableLogic
     private Long isDelete;
     /**
      * 创建时间

+ 36 - 4
nightFragrance-massage/src/main/java/com/ylx/massage/domain/MaTechnician.java

@@ -86,9 +86,6 @@ public class MaTechnician extends BaseEntity {
      */
     private String teAreaCode;
 
-    /**
-     * 年龄
-     */
     /**
      * 年龄
      */
@@ -235,7 +232,7 @@ public class MaTechnician extends BaseEntity {
     private String merchantStatus;
 
     /**
-     * 商户接单状态(0休息中1在线接单)
+     * 商户接单状态(0:休息中 1:在线接单)
      */
     @Excel(name = "商户接单状态(0休息中 1在线接单)")
     private Integer postState;
@@ -302,4 +299,39 @@ public class MaTechnician extends BaseEntity {
     @ApiModelProperty("是否推荐")
     private Integer isRecommend;
 
+    /**
+     * 省级行政区编码,如:110000。
+     */
+    @TableField(exist = false)
+    private String provinceCode;
+
+    /**
+     * 省级行政区名称,如:北京市。
+     */
+    @TableField(exist = false)
+    private String provinceName;
+
+    /**
+     * 地级市编码,如:110100。
+     */
+    @TableField(exist = false)
+    private String cityCode;
+
+    /**
+     * 地级市名称,如:北京市。
+     */
+    @TableField(exist = false)
+    private String cityName;
+
+    /**
+     * 运营中心ID。
+     */
+    @TableField(exist = false)
+    private Integer operationCenterId;
+
+    /**
+     * 运营中心名称。
+     */
+    @TableField(exist = false)
+    private String operationCenterName;
 }

+ 134 - 96
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/MaTechnicianServiceImpl.java

@@ -1,7 +1,6 @@
 package com.ylx.massage.service.impl;
 
 import java.math.BigDecimal;
-import java.text.SimpleDateFormat;
 import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -1136,7 +1135,22 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
     private MaTechnician findMerchantByOpenid(String openid) {
         LambdaQueryWrapper<MaTechnician> query = new LambdaQueryWrapper<>();
         query.eq(MaTechnician::getCOpenid, openid);
-        return maTechnicianMapper.selectOne(query);
+        MaTechnician merchant = maTechnicianMapper.selectOne(query);
+        // 根据商户ID查询city_operation_application表
+        if (merchant != null && merchant.getId() != null) {
+            LambdaQueryWrapper<CityOperationApplication> query1 = new LambdaQueryWrapper<>();
+            query1.eq(CityOperationApplication::getMerchantId, merchant.getId());
+            CityOperationApplication application = cityOperationApplicationMapper.selectOne(query1);
+            if (application != null) {
+                merchant.setProvinceCode(application.getProvinceCode());
+                merchant.setProvinceName(application.getProvinceName());
+                merchant.setCityCode(application.getCityCode());
+                merchant.setCityName(application.getCityName());
+                merchant.setOperationCenterId(application.getOperationCenterId());
+                merchant.setOperationCenterName(application.getOperationCenterName());
+            }
+        }
+        return merchant;
     }
 
     private MaTechnician findMerchantById(Integer userId) {
@@ -1286,99 +1300,131 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
      * @return Result
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public Result switchToOffline(Long userId, Boolean forceConfirm) {
+        if (userId == null) {
+            throw new ServiceException("商户ID不能为空");
+        }
         MaTechnician technician = getTechnician(userId);
+        if (technician == null) {
+            throw new ServiceException("商户不存在");
+        }
 
-        // 1. 基础权限校验 (对应流程图左下角)
         if (!hasActiveSkills(userId)) {
-            throw new RuntimeException("请先申请开通技能");
+            throw new ServiceException("请先申请开通技能");
         }
         if (!hasHomeAddress(userId)) {
-            // 这里通常会触发前端弹窗要求添加地址,或者直接阻断
-            throw new RuntimeException("请完善家庭地址");
+            throw new ServiceException("请完善家庭地址");
+        }
+        // 由在线接单切换为休息状态
+        if (TechnicianStatusEnum.ONLINE.getCode().equals(technician.getPostState())) {
+            return switchOnlineToResting(userId, technician, Boolean.TRUE.equals(forceConfirm));
+        }
+        // 切换到在线接单状态
+        switchRestingToOnline(userId, technician);
+        return Result.ok("状态已切换成功");
+    }
+
+    /**
+     * 状态切换:下线休息
+     *
+     * @param userId
+     * @param technician
+     * @param forceConfirm
+     * @return Result
+     */
+    private Result switchOnlineToResting(Long userId, MaTechnician technician, boolean forceConfirm) {
+        if (JsStatusEnum.JS_SERVICE.getCode().equals(technician.getServiceState())) {
+            throw new ServiceException("您有服务中的订单,不能下岗");
         }
-        // 服务标签:按摩推拿
-        if (ProjectCategoryEnum.MASSAGE.getCode().equals(technician.getServiceTag())) {
-            // 2. 状态判断逻辑 (对应流程图中间的菱形判断)
-            // 只有处于“在线接单”状态才需要检查疲劳度/时长限制
-            if (TechnicianStatusEnum.ONLINE.getCode().equals(technician.getPostState())) {
-                if (technician.getServiceState().equals(JsStatusEnum.JS_SERVICE.getCode())) {
-                    throw new ServiceException("您有服务中的订单,不能下岗");
-                }
-                // 获取今日的商户考勤记录
-                MerchantDailyAttendance attendance = getTodayAttendance(userId);
-
-                long minutesOnline = 0;
-                if (attendance != null && attendance.getAttendanceStartTime() != null) {
-                    LocalDateTime localDateTime = attendance.getAttendanceStartTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
-                    //计算截止现在的时长,单位为分钟
-                    minutesOnline = Duration.between(localDateTime, LocalDateTime.now()).toMinutes();
-                    // 计算今日的累加在线时长
-                    minutesOnline = minutesOnline + getWorkDuration(userId);
-                }
-                AttendanceRule rule = getAttendanceRule();
-                if (rule != null) {
-                    // 将小时转换为分钟进行比较
-                    BigDecimal minutes = rule.getBasicWorkHours().multiply(new BigDecimal(60));
-                    // 2. 精确转换成 long(无小数、无溢出才成功)
-                    long requiredMinutes = minutes.longValueExact();
-                    // 判断是否超过了平台规定的在线时间 (X小时)
-                    if (minutesOnline < requiredMinutes) {
-                        // 情况: 未满足在线时间,且用户还没有点击“确认下线”(forceConfirm=false)
-                        if (!forceConfirm) {
-                            // 返回特定错误码或数据结构,告诉前端弹出“我在想想/确认下线”的模态框
-                            return Result.ok("平台对您的在线时间做了约定,每日在线需满足"
-                                    + (requiredMinutes / 60) + "小时,距离您下线时间还剩余" + ((requiredMinutes / 60) - minutesOnline / 60) + "小时"
-                                    + "不满足在线时间将收到平台处罚,是否确认下线?");
-                        }
-                    } else {
-                        // 情况: 已满足在线时间,且用户点击点击了“确认下线”,允许通过
-                        return Result.ok("状态已切换成功");
-                    }
-                }
-            }
 
-            // 3. 执行状态更新 (更新为休息中状态)
-            updateStatus(userId, TechnicianStatusEnum.RESTING);
-            if (!forceConfirm) {
-                // 格式化成 yyyy-MM-dd 纯日期字符串
-                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
-                String today = sdf.format(new Date());
-
-                // 查询商户今日的最近考勤记录
-                LambdaQueryWrapper<MerchantDailyAttendance> query = new LambdaQueryWrapper<>();
-                query.eq(MerchantDailyAttendance::getMerchantId, userId)
-                        .eq(MerchantDailyAttendance::getAttendanceDate, today)
-                        .orderByDesc(MerchantDailyAttendance::getCreateTime);
-                MerchantDailyAttendance update = merchantDailyAttendanceMapper.selectOne(query);
-                if (update != null) {
-                    LocalDateTime localDateTime = update.getAttendanceStartTime().toInstant()
-                            .atZone(ZoneId.systemDefault())
-                            .toLocalDateTime();
-                    LambdaUpdateWrapper<MerchantDailyAttendance> updateWrapper = new LambdaUpdateWrapper<>();
-                    updateWrapper.eq(MerchantDailyAttendance::getId, update.getId())
-                            .set(MerchantDailyAttendance::getAttendanceEndTime, DateUtils.getNowDate())
-                            .set(MerchantDailyAttendance::getTotalWorkMinutes, Duration.between(localDateTime, LocalDateTime.now()).toMinutes())
-                            .set(MerchantDailyAttendance::getUpdateTime, DateUtils.getNowDate());
-                    merchantDailyAttendanceMapper.update(update, updateWrapper);
+        MerchantDailyAttendance currentAttendance = getTodayAttendance(userId);
+        if (ProjectCategoryEnum.MASSAGE.getCode().equals(technician.getServiceTag())) {
+            AttendanceRule rule = getAttendanceRule();
+            if (rule != null && rule.getBasicWorkHours() != null) {
+                long minutesOnline = calculateTodayOnlineMinutes(userId, currentAttendance);
+                long requiredMinutes = rule.getBasicWorkHours().multiply(BigDecimal.valueOf(60)).longValue();
+                if (minutesOnline < requiredMinutes && !forceConfirm) {
+                    return Result.ok(buildOfflineConfirmMessage(requiredMinutes, minutesOnline));
                 }
             }
-        } else {
-            //更新状态为在线接单
-            updateStatus(userId, TechnicianStatusEnum.ONLINE);
-            // 插入今日考勤记录
-            MerchantDailyAttendance merchantDailyAttendance = new MerchantDailyAttendance()
-                    .setMerchantId(userId.intValue())
-                    .setAttendanceDate(DateUtils.getNowDate())
-                    .setMerchantName(technician.getTeName())
-                    .setAttendanceStartTime(DateUtils.getNowDate())
-                    .setCreateBy(technician.getTeName())
-                    .setCreateTime(LocalDateTime.now());
-            merchantDailyAttendanceMapper.insert(merchantDailyAttendance);
         }
+
+        updateStatus(userId, TechnicianStatusEnum.RESTING);
+        closeTodayAttendance(currentAttendance);
         return Result.ok("状态已切换成功");
     }
 
+    private void switchRestingToOnline(Long userId, MaTechnician technician) {
+        updateStatus(userId, TechnicianStatusEnum.ONLINE);
+        MerchantDailyAttendance merchantDailyAttendance = new MerchantDailyAttendance()
+                .setMerchantId(userId.intValue())
+                .setAttendanceDate(DateUtils.getNowDate())
+                .setMerchantName(technician.getTeName())
+                .setAttendanceStartTime(DateUtils.getNowDate())
+                .setCreateBy(technician.getTeName())
+                .setCreateTime(LocalDateTime.now());
+        merchantDailyAttendanceMapper.insert(merchantDailyAttendance);
+    }
+
+    /**
+     * 计算商户今天在线时间(分钟)
+     *
+     * @param userId
+     * @param currentAttendance
+     * @return long
+     */
+    private long calculateTodayOnlineMinutes(Long userId, MerchantDailyAttendance currentAttendance) {
+        long totalMinutes = getWorkDuration(userId, currentAttendance == null ? null : currentAttendance.getId());
+        if (currentAttendance == null || currentAttendance.getAttendanceStartTime() == null) {
+            return totalMinutes;
+        }
+        LocalDateTime startTime = toLocalDateTime(currentAttendance.getAttendanceStartTime());
+        return totalMinutes + Math.max(0L, Duration.between(startTime, LocalDateTime.now()).toMinutes());
+    }
+
+    /**
+     * 构建下线确认消息
+     *
+     * @param requiredMinutes
+     * @param minutesOnline
+     * @return String
+     */
+    private String buildOfflineConfirmMessage(long requiredMinutes, long minutesOnline) {
+        long remainMinutes = Math.max(0L, requiredMinutes - minutesOnline);
+        long remainHours = remainMinutes / 60;
+        long remainMinutePart = remainMinutes % 60;
+        String remainText = remainMinutePart == 0
+                ? remainHours + "小时"
+                : remainHours + "小时" + remainMinutePart + "分钟";
+        return "平台对您的在线时间做了约定,每日在线需满足"
+                + (requiredMinutes / 60) + "小时,距离您下线时间还剩余" + remainText
+                + "不满足在线时间将收到平台处罚,是否确认下线?";
+    }
+
+    /**
+     * 关闭今天商户的考勤记录
+     *
+     * @param attendance
+     */
+    private void closeTodayAttendance(MerchantDailyAttendance attendance) {
+        if (attendance == null || attendance.getId() == null || attendance.getAttendanceStartTime() == null) {
+            return;
+        }
+        LocalDateTime startTime = toLocalDateTime(attendance.getAttendanceStartTime());
+        long workMinutes = Math.max(0L, Duration.between(startTime, LocalDateTime.now()).toMinutes());
+        LambdaUpdateWrapper<MerchantDailyAttendance> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(MerchantDailyAttendance::getId, attendance.getId())
+                .set(MerchantDailyAttendance::getAttendanceEndTime, DateUtils.getNowDate())
+                .set(MerchantDailyAttendance::getTotalWorkMinutes, Math.toIntExact(workMinutes))
+                .set(MerchantDailyAttendance::getUpdateTime, DateUtils.getNowDate());
+        merchantDailyAttendanceMapper.update(attendance, updateWrapper);
+    }
+
+    private LocalDateTime toLocalDateTime(Date date) {
+        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+    }
+
     /**
      * 获取今天商户的考勤记录
      *
@@ -1398,20 +1444,20 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
      * 获取商户的累计工作时长
      *
      * @param userId 技师ID
+     * @param excludeAttendanceId 排除的考勤记录ID
      * @return 工作时长(分钟)
      */
-    private long getWorkDuration(Long userId) {
+    private long getWorkDuration(Long userId, Long excludeAttendanceId) {
         LambdaQueryWrapper<MerchantDailyAttendance> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(MerchantDailyAttendance::getMerchantId, userId)
                 .eq(MerchantDailyAttendance::getAttendanceDate, DateUtils.getNowDate());
         List<MerchantDailyAttendance> attendances = merchantDailyAttendanceMapper.selectList(wrapper);
         if (attendances == null || attendances.isEmpty()) return 0;
-        // 计算指定日期的总分钟数
-        long totalMinutes = attendances.stream()
-                .filter(attendance -> DateUtils.getNowDate().toString().equals(attendance.getAttendanceDate().toString()))
+        return attendances.stream()
+                .filter(attendance -> excludeAttendanceId == null || !excludeAttendanceId.equals(attendance.getId()))
+                .filter(attendance -> attendance.getTotalWorkMinutes() != null)
                 .mapToLong(MerchantDailyAttendance::getTotalWorkMinutes)
                 .sum();
-        return totalMinutes;
     }
 
     /**
@@ -1434,11 +1480,8 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
      * @return boolean true: 有可用技能, false: 无
      */
     public boolean hasActiveSkills(Long userId) {
-        if (userId == null) return false;
-        // 构建查询条件:用户ID匹配 AND 状态为已发布/生效中
         LambdaQueryWrapper<MaProject> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(MaProject::getMerchantId, userId)
-                .eq(MaProject::getAuditStatus, 1); // 假设 1 代表 "生效/已审核"
+        wrapper.eq(MaProject::getMerchantId, userId).eq(MaProject::getAuditStatus, 1);
         // 只要查到一条记录即返回 true
         return maProjectMapper.selectCount(wrapper) > 0;
     }
@@ -1450,13 +1493,8 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
      * @return boolean true: 有地址, false: 无
      */
     public boolean hasHomeAddress(Long userId) {
-        if (userId == null) return false;
-        // 构建查询条件:用户ID匹配 AND 是默认地址(可选) AND 未删除
         LambdaQueryWrapper<TAddress> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(TAddress::getMerchantId, userId)
-                .eq(TAddress::getUserType, 2)  // 商户类型
-                .eq(TAddress::getIsDelete, 0);  // 逻辑未删除
-
+        wrapper.eq(TAddress::getMerchantId, userId).eq(TAddress::getUserType, 2);
         // 统计数量是否大于0
         long count = addressMapper.selectCount(wrapper);
         return count > 0;

+ 0 - 289
nightFragrance-massage/src/test/java/com/ylx/massage/service/impl/MaTechnicianServiceImplTest.java

@@ -1,289 +0,0 @@
-package com.ylx.massage.service.impl;
-
-import com.baomidou.mybatisplus.core.MybatisConfiguration;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
-import com.ylx.massage.domain.MaTechnician;
-import com.ylx.massage.domain.MerchantApplyFile;
-import com.ylx.massage.domain.dto.MerchantApplyFileDto;
-import com.ylx.massage.domain.dto.MerchantApplyFileRequestDto;
-import com.ylx.massage.domain.vo.MerchantAuditFile;
-import com.ylx.massage.mapper.MaTechnicianMapper;
-import com.ylx.massage.mapper.MerchantApplyFileMapper;
-import org.apache.ibatis.builder.MapperBuilderAssistant;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import java.lang.reflect.Field;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-public class MaTechnicianServiceImplTest {
-
-    @BeforeAll
-    public static void initMybatisPlusTableInfo() {
-        MybatisConfiguration configuration = new MybatisConfiguration();
-        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), MaTechnician.class);
-        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), MerchantApplyFile.class);
-    }
-
-    @Test
-    public void updateTechnicianOnlyUpdatesNicknameAndBriefForBaseInfo() {
-        MaTechnicianMapper maTechnicianMapper = mock(MaTechnicianMapper.class);
-        MerchantApplyFileMapper merchantApplyFileMapper = mock(MerchantApplyFileMapper.class);
-        when(maTechnicianMapper.update(isNull(), any(LambdaUpdateWrapper.class))).thenReturn(1);
-        MaTechnicianServiceImpl service = buildService(maTechnicianMapper, merchantApplyFileMapper);
-
-        MerchantApplyFileRequestDto request = new MerchantApplyFileRequestDto();
-        MaTechnician technician = new MaTechnician();
-        technician.setId(7);
-        technician.setTeName("不能修改的姓名");
-        technician.setTePhone("18800000000");
-        technician.setAvatar("不能修改的形象照");
-        technician.setTeNickName("新昵称");
-        technician.setTeBrief("新简介");
-        request.setTechnician(technician);
-
-        service.updateTechnician(request);
-
-        ArgumentCaptor<LambdaUpdateWrapper<MaTechnician>> wrapperCaptor = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
-        verify(maTechnicianMapper).update(isNull(), wrapperCaptor.capture());
-        String sqlSet = String.join(",", wrapperCaptor.getValue().getSqlSet());
-        assertTrue(sqlSet.contains("te_nick_name"));
-        assertTrue(sqlSet.contains("te_brief"));
-        assertFalse(sqlSet.contains("te_name"));
-        assertFalse(sqlSet.contains("te_phone"));
-        assertFalse(sqlSet.contains("avatar"));
-        assertFalse(sqlSet.contains("te_avatar"));
-    }
-
-    @Test
-    public void updateTechnicianUpsertsApplyFilesByMerchantAndFileType() {
-        MaTechnicianMapper maTechnicianMapper = mock(MaTechnicianMapper.class);
-        MerchantApplyFileMapper merchantApplyFileMapper = mock(MerchantApplyFileMapper.class);
-        when(maTechnicianMapper.update(isNull(), any(LambdaUpdateWrapper.class))).thenReturn(1);
-        MerchantApplyFile existsFile = new MerchantApplyFile();
-        existsFile.setId(3L);
-        existsFile.setMerchantId(7);
-        existsFile.setFileType("1");
-        when(merchantApplyFileMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(existsFile, null);
-        MaTechnicianServiceImpl service = buildService(maTechnicianMapper, merchantApplyFileMapper);
-
-        MerchantApplyFileRequestDto request = new MerchantApplyFileRequestDto();
-        MaTechnician technician = new MaTechnician();
-        technician.setId(7);
-        technician.setTeNickName("新昵称");
-        technician.setTeBrief("新简介");
-        request.setTechnician(technician);
-
-        MerchantApplyFileDto updateFile = buildFile("1", "new-image.jpg", "https://file/new-image.jpg");
-        MerchantApplyFileDto insertFile = buildFile("2", "life.jpg", "https://file/life.jpg");
-        request.setReq(Arrays.asList(updateFile, insertFile));
-
-        service.updateTechnician(request);
-
-        ArgumentCaptor<MerchantApplyFile> updateCaptor = ArgumentCaptor.forClass(MerchantApplyFile.class);
-        verify(merchantApplyFileMapper).updateById(updateCaptor.capture());
-        MerchantApplyFile updatedFile = updateCaptor.getValue();
-        assertEquals(3L, updatedFile.getId());
-        assertEquals(7, updatedFile.getMerchantId());
-        assertEquals("1", updatedFile.getFileType());
-        assertEquals("new-image.jpg", updatedFile.getFileName());
-        assertEquals("https://file/new-image.jpg", updatedFile.getFileUrl());
-        assertEquals("7", updatedFile.getUpdateBy());
-        assertEquals(0, updatedFile.getIsDelete());
-
-        ArgumentCaptor<MerchantApplyFile> insertCaptor = ArgumentCaptor.forClass(MerchantApplyFile.class);
-        verify(merchantApplyFileMapper).insert(insertCaptor.capture());
-        MerchantApplyFile insertedFile = insertCaptor.getValue();
-        assertEquals(7, insertedFile.getMerchantId());
-        assertEquals("2", insertedFile.getFileType());
-        assertEquals("life.jpg", insertedFile.getFileName());
-        assertEquals("https://file/life.jpg", insertedFile.getFileUrl());
-        assertEquals("7", insertedFile.getCreateBy());
-        assertEquals("7", insertedFile.getUpdateBy());
-        assertEquals(0, insertedFile.getIsDelete());
-    }
-
-    @Test
-    public void merchantApplyFileDtoDoesNotExposeMerchantIdInRequestPayload() {
-        for (Field field : MerchantApplyFileDto.class.getDeclaredFields()) {
-            assertNotEquals("merchantId", field.getName());
-        }
-    }
-
-    @Test
-    public void applyFileReplacesRequestedTypesAndInsertsAllFilesInGroups() {
-        MaTechnicianMapper maTechnicianMapper = mock(MaTechnicianMapper.class);
-        MerchantApplyFileMapper merchantApplyFileMapper = mock(MerchantApplyFileMapper.class);
-        when(maTechnicianMapper.update(isNull(), any(LambdaUpdateWrapper.class))).thenReturn(1);
-        MaTechnicianServiceImpl service = buildService(maTechnicianMapper, merchantApplyFileMapper);
-
-        MerchantApplyFileRequestDto request = new MerchantApplyFileRequestDto();
-        MaTechnician technician = new MaTechnician();
-        technician.setId(7);
-        technician.setTeNickName("新昵称");
-        technician.setTeBrief("新简介");
-        request.setTechnician(technician);
-        request.setReq(Arrays.asList(
-                buildFileGroup("1",
-                        buildFile(null, "portrait-1.jpg", "https://file/portrait-1.jpg"),
-                        buildFile(null, "portrait-2.jpg", "https://file/portrait-2.jpg")),
-                buildFileGroup("3",
-                        buildFile(null, "video.mp4", "https://file/video.mp4"))
-        ));
-
-        service.applyFile(request);
-
-        verify(merchantApplyFileMapper, times(2)).delete(any(LambdaQueryWrapper.class));
-        ArgumentCaptor<MerchantApplyFile> insertCaptor = ArgumentCaptor.forClass(MerchantApplyFile.class);
-        verify(merchantApplyFileMapper, times(3)).insert(insertCaptor.capture());
-        List<MerchantApplyFile> insertedFiles = insertCaptor.getAllValues();
-        assertEquals("1", insertedFiles.get(0).getFileType());
-        assertEquals("portrait-1.jpg", insertedFiles.get(0).getFileName());
-        assertEquals("1", insertedFiles.get(1).getFileType());
-        assertEquals("portrait-2.jpg", insertedFiles.get(1).getFileName());
-        assertEquals("3", insertedFiles.get(2).getFileType());
-        assertEquals("video.mp4", insertedFiles.get(2).getFileName());
-        insertedFiles.forEach(file -> {
-            assertEquals(7, file.getMerchantId());
-            assertEquals("7", file.getCreateBy());
-            assertEquals("7", file.getUpdateBy());
-            assertEquals(0, file.getIsDelete());
-        });
-    }
-
-    @Test
-    public void applyFileUsesTechnicianIdWhenReqHasNoMerchantId() {
-        MaTechnicianMapper maTechnicianMapper = mock(MaTechnicianMapper.class);
-        MerchantApplyFileMapper merchantApplyFileMapper = mock(MerchantApplyFileMapper.class);
-        when(maTechnicianMapper.update(isNull(), any(LambdaUpdateWrapper.class))).thenReturn(1);
-        MaTechnicianServiceImpl service = buildService(maTechnicianMapper, merchantApplyFileMapper);
-
-        MerchantApplyFileRequestDto request = new MerchantApplyFileRequestDto();
-        MaTechnician technician = new MaTechnician();
-        technician.setId(7);
-        technician.setTeNickName("新昵称");
-        technician.setTeBrief("新简介");
-        request.setTechnician(technician);
-        request.setReq(Collections.singletonList(
-                buildFileGroup("2",
-                        buildFile(null, "life.jpg", "https://file/life.jpg"))
-        ));
-
-        service.applyFile(request);
-
-        ArgumentCaptor<MerchantApplyFile> insertCaptor = ArgumentCaptor.forClass(MerchantApplyFile.class);
-        verify(merchantApplyFileMapper).insert(insertCaptor.capture());
-        MerchantApplyFile insertedFile = insertCaptor.getValue();
-        assertEquals(7, insertedFile.getMerchantId());
-        assertEquals("2", insertedFile.getFileType());
-        assertEquals("life.jpg", insertedFile.getFileName());
-        assertEquals("7", insertedFile.getCreateBy());
-        assertEquals("7", insertedFile.getUpdateBy());
-        assertEquals(0, insertedFile.getIsDelete());
-    }
-
-    @Test
-    public void getTechnicianInfoFindsMerchantByOpenidWithFiles() {
-        MaTechnicianMapper maTechnicianMapper = mock(MaTechnicianMapper.class);
-        MerchantApplyFileMapper merchantApplyFileMapper = mock(MerchantApplyFileMapper.class);
-        MaTechnician merchant = buildMerchant(7, "openid-7");
-        MerchantApplyFile file = buildApplyFile(7, "1");
-        when(maTechnicianMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(merchant);
-        when(merchantApplyFileMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.singletonList(file));
-        MaTechnicianServiceImpl service = buildService(maTechnicianMapper, merchantApplyFileMapper);
-
-        MerchantAuditFile result = service.getTechnicianInfo("openid-7");
-
-        assertEquals(merchant, result.getMerchant());
-        assertEquals(1, result.getMerchantAuditFile().size());
-        assertEquals(file, result.getMerchantAuditFile().get(0));
-    }
-
-    @Test
-    public void getTechnicianListFindsMerchantByUserIdWithFiles() {
-        MaTechnicianMapper maTechnicianMapper = mock(MaTechnicianMapper.class);
-        MerchantApplyFileMapper merchantApplyFileMapper = mock(MerchantApplyFileMapper.class);
-        MaTechnician merchant = buildMerchant(7, "openid-7");
-        MerchantApplyFile file = buildApplyFile(7, "1");
-        when(maTechnicianMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(merchant);
-        when(merchantApplyFileMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.singletonList(file));
-        MaTechnicianServiceImpl service = buildService(maTechnicianMapper, merchantApplyFileMapper);
-
-        MerchantAuditFile result = service.getTechnicianList(7);
-
-        assertEquals(merchant, result.getMerchant());
-        assertEquals(1, result.getMerchantAuditFile().size());
-        assertEquals(file, result.getMerchantAuditFile().get(0));
-        ArgumentCaptor<LambdaQueryWrapper<MerchantApplyFile>> fileQueryCaptor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
-        verify(merchantApplyFileMapper).selectList(fileQueryCaptor.capture());
-        String sqlSegment = fileQueryCaptor.getValue().getSqlSegment();
-        assertTrue(sqlSegment.contains("merchant_id"));
-    }
-
-    @Test
-    public void getTechnicianInfoRejectsBlankOpenid() {
-        MaTechnicianMapper maTechnicianMapper = mock(MaTechnicianMapper.class);
-        MerchantApplyFileMapper merchantApplyFileMapper = mock(MerchantApplyFileMapper.class);
-        MaTechnicianServiceImpl service = buildService(maTechnicianMapper, merchantApplyFileMapper);
-
-        assertThrows(IllegalArgumentException.class, () -> service.getTechnicianInfo(" "));
-    }
-
-    private MaTechnicianServiceImpl buildService(MaTechnicianMapper maTechnicianMapper,
-                                                 MerchantApplyFileMapper merchantApplyFileMapper) {
-        MaTechnicianServiceImpl service = new MaTechnicianServiceImpl();
-        ReflectionTestUtils.setField(service, "maTechnicianMapper", maTechnicianMapper);
-        ReflectionTestUtils.setField(service, "merchantApplyFileMapper", merchantApplyFileMapper);
-        return service;
-    }
-
-    private MaTechnician buildMerchant(Integer id, String openid) {
-        MaTechnician merchant = new MaTechnician();
-        merchant.setId(id);
-        merchant.setCOpenid(openid);
-        merchant.setTeNickName("merchant-" + id);
-        return merchant;
-    }
-
-    private MerchantApplyFile buildApplyFile(Integer merchantId, String fileType) {
-        MerchantApplyFile file = new MerchantApplyFile();
-        file.setMerchantId(merchantId);
-        file.setFileType(fileType);
-        return file;
-    }
-
-    private MerchantApplyFileDto buildFile(String fileType, String fileName, String fileUrl) {
-        MerchantApplyFileDto file = new MerchantApplyFileDto();
-        file.setFileType(fileType);
-        file.setFileName(fileName);
-        file.setFileUrl(fileUrl);
-        file.setFileSize(1024L);
-        file.setContentType("image/jpeg");
-        return file;
-    }
-
-    private MerchantApplyFileDto buildFileGroup(String fileType, MerchantApplyFileDto... files) {
-        MerchantApplyFileDto group = new MerchantApplyFileDto();
-        group.setFileType(fileType);
-        group.setFiles(Arrays.asList(files));
-        return group;
-    }
-}