| | |
| | | import java.math.BigDecimal; |
| | | |
| | | public interface GuideUserMapper extends BaseMapper<GuideUser> { |
| | | @Update("UPDATE guide_user SET username = #{username}, mobile = NULL, certificate_no = NULL, " + |
| | | "id_card_no = NULL, id_card_front_url = NULL, id_card_back_url = NULL, status = 3, " + |
| | | "auto_disable_whitelisted = 0, auto_disabled = 0, updated_at = NOW() " + |
| | | "WHERE id = #{guideId} AND deleted = 0") |
| | | int cancelAccount(@Param("guideId") Long guideId, @Param("username") String username); |
| | | |
| | | @Update("UPDATE guide_user SET username = #{username}, mobile = NULL, certificate_no = NULL, " + |
| | | "id_card_no = NULL, id_card_front_url = NULL, id_card_back_url = NULL, updated_at = NOW() " + |
| | | "WHERE id = #{guideId} AND status = 3 AND deleted = 0") |
| | | int anonymizeCancelledGuide(@Param("guideId") Long guideId, @Param("username") String username); |
| | | |
| | | @Update("UPDATE guide_user SET id_card_no = NULL, id_card_front_url = NULL, " + |
| | | "id_card_back_url = NULL, updated_at = NOW() " + |
| | | "WHERE id_card_no = #{idCardNo} AND status = 3 AND deleted = 0") |
| | | int clearCancelledIdentityByIdCard(@Param("idCardNo") String idCardNo); |
| | | |
| | | @Select("SELECT * FROM guide_user WHERE id = #{guideId} AND deleted = 0 FOR UPDATE") |
| | | GuideUser selectForUpdate(@Param("guideId") Long guideId); |
| | | |
| | |
| | | package com.tourguide.modules.user.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.tourguide.common.exception.BusinessException; |
| | | import com.tourguide.modules.order.domain.OrderStatus; |
| | | import com.tourguide.modules.order.entity.TourOrder; |
| | |
| | | if (idCardNo != null && !idCardNo.trim().isEmpty()) { |
| | | guideApplicationMapper.clearIdentityByIdCard(idCardNo); |
| | | } |
| | | guideUserMapper.update(null, new LambdaUpdateWrapper<GuideUser>() |
| | | .eq(GuideUser::getId, guideId) |
| | | .set(GuideUser::getUsername, "deleted_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24)) |
| | | .set(GuideUser::getMobile, null) |
| | | .set(GuideUser::getCertificateNo, null) |
| | | .set(GuideUser::getIdCardNo, null) |
| | | .set(GuideUser::getIdCardFrontUrl, null) |
| | | .set(GuideUser::getIdCardBackUrl, null) |
| | | .set(GuideUser::getStatus, GuideUserService.STATUS_CANCELLED) |
| | | .set(GuideUser::getAutoDisableWhitelisted, 0) |
| | | .set(GuideUser::getAutoDisabled, 0)); |
| | | String deletedUsername = "deleted_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24); |
| | | guideUserMapper.cancelAccount(guideId, deletedUsername); |
| | | tokenSessionService.remove(payload); |
| | | } |
| | | } |
| | |
| | | package com.tourguide.modules.user.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.tourguide.common.api.PageResponse; |
| | | import com.tourguide.common.exception.BusinessException; |
| | |
| | | } |
| | | |
| | | private void anonymizeCancelledGuide(GuideUser user) { |
| | | userMapper.update(null, new LambdaUpdateWrapper<GuideUser>() |
| | | .eq(GuideUser::getId, user.getId()) |
| | | .set(GuideUser::getUsername, "deleted_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24)) |
| | | .set(GuideUser::getMobile, null) |
| | | .set(GuideUser::getCertificateNo, null) |
| | | .set(GuideUser::getIdCardNo, null)); |
| | | String deletedUsername = "deleted_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24); |
| | | userMapper.anonymizeCancelledGuide(user.getId(), deletedUsername); |
| | | } |
| | | |
| | | private void ensureUniqueGuideIdentity(GuideApplication application, GuideUser existingUser) { |
| | | if (StringUtils.hasText(application.getIdCardNo())) { |
| | | // 兼容旧注销数据:释放已注销账号占用的身份证唯一索引。 |
| | | userMapper.clearCancelledIdentityByIdCard(application.getIdCardNo().trim().toUpperCase()); |
| | | } |
| | | if (StringUtils.hasText(application.getCertificateNo()) |
| | | && userMapper.selectCount(new LambdaQueryWrapper<GuideUser>() |
| | | .eq(GuideUser::getCertificateNo, application.getCertificateNo()) |
| | | .ne(GuideUser::getStatus, STATUS_CANCELLED) |
| | | .ne(existingUser != null && existingUser.getId() != null, |
| | | GuideUser::getId, existingUser == null ? null : existingUser.getId())) > 0) { |
| | | throw new BusinessException(409, "该导游证号已存在导游账号"); |
| | | } |
| | | if (StringUtils.hasText(application.getIdCardNo()) |
| | | && userMapper.selectCount(new LambdaQueryWrapper<GuideUser>() |
| | | .eq(GuideUser::getIdCardNo, application.getIdCardNo()) |
| | | .eq(GuideUser::getIdCardNo, application.getIdCardNo().trim().toUpperCase()) |
| | | .ne(GuideUser::getStatus, STATUS_CANCELLED) |
| | | .ne(existingUser != null && existingUser.getId() != null, |
| | | GuideUser::getId, existingUser == null ? null : existingUser.getId())) > 0) { |
| | | throw new BusinessException(409, "该身份证号已存在导游账号"); |
| | |
| | | import com.tourguide.security.TokenSessionService; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.ArgumentCaptor; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.never; |
| | | import static org.mockito.Mockito.verify; |
| | |
| | | .isInstanceOf(BusinessException.class) |
| | | .hasMessage("存在未完成订单,暂时无法注销账号"); |
| | | |
| | | verify(guideUserMapper, never()).updateById(any()); |
| | | verify(guideUserMapper, never()).cancelAccount(any(), any()); |
| | | verify(tokenSessionService, never()).remove(payload); |
| | | } |
| | | |
| | |
| | | |
| | | accountService.cancel(8L, "guide-token"); |
| | | |
| | | ArgumentCaptor<GuideUser> userCaptor = ArgumentCaptor.forClass(GuideUser.class); |
| | | verify(guideUserMapper).updateById(userCaptor.capture()); |
| | | assertThat(userCaptor.getValue().getStatus()).isEqualTo(GuideUserService.STATUS_CANCELLED); |
| | | assertThat(userCaptor.getValue().getAutoDisableWhitelisted()).isZero(); |
| | | assertThat(userCaptor.getValue().getUsername()).startsWith("deleted_").hasSize(32); |
| | | assertThat(userCaptor.getValue().getMobile()).isNull(); |
| | | assertThat(userCaptor.getValue().getCertificateNo()).isNull(); |
| | | assertThat(userCaptor.getValue().getIdCardNo()).isNull(); |
| | | assertThat(userCaptor.getValue().getIdCardFrontUrl()).isNull(); |
| | | assertThat(userCaptor.getValue().getIdCardBackUrl()).isNull(); |
| | | verify(guideUserMapper).cancelAccount(eq(8L), org.mockito.ArgumentMatchers.argThat( |
| | | username -> username.startsWith("deleted_") && username.length() == 32)); |
| | | verify(guideApplicationMapper).clearIdentityByIdCard("11010519491231002X"); |
| | | verify(tokenSessionService).remove(payload); |
| | | } |
| | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.verify; |
| | | import static org.mockito.Mockito.when; |
| | |
| | | |
| | | service.createPendingFromApplication(application); |
| | | |
| | | ArgumentCaptor<GuideUser> updateCaptor = ArgumentCaptor.forClass(GuideUser.class); |
| | | verify(userMapper).updateById(updateCaptor.capture()); |
| | | GuideUser anonymizedGuide = updateCaptor.getValue(); |
| | | assertThat(anonymizedGuide.getStatus()).isEqualTo(GuideUserService.STATUS_CANCELLED); |
| | | assertThat(anonymizedGuide.getUsername()).startsWith("deleted_").hasSize(32); |
| | | assertThat(anonymizedGuide.getMobile()).isNull(); |
| | | assertThat(anonymizedGuide.getCertificateNo()).isNull(); |
| | | assertThat(anonymizedGuide.getIdCardNo()).isNull(); |
| | | verify(userMapper).anonymizeCancelledGuide(eq(12L), org.mockito.ArgumentMatchers.argThat( |
| | | username -> username.startsWith("deleted_") && username.length() == 32)); |
| | | verify(userMapper).clearCancelledIdentityByIdCard("11010519491231002X"); |
| | | |
| | | ArgumentCaptor<GuideUser> insertCaptor = ArgumentCaptor.forClass(GuideUser.class); |
| | | verify(userMapper).insert(insertCaptor.capture()); |
| | |
| | | } |
| | | |
| | | @Test |
| | | void shouldReleaseIdentityFromStaleCancelledGuideBeforeNewRegistration() { |
| | | GuideApplication application = new GuideApplication(); |
| | | application.setMobile("13900139000"); |
| | | application.setRealName("新导游"); |
| | | application.setIdCardNo("11010519491231002X"); |
| | | when(userMapper.selectOne(any())).thenReturn(null); |
| | | |
| | | service.createPendingFromApplication(application); |
| | | |
| | | verify(userMapper).clearCancelledIdentityByIdCard("11010519491231002X"); |
| | | verify(userMapper).insert(any(GuideUser.class)); |
| | | } |
| | | |
| | | @Test |
| | | void shouldTreatOneYearOrLessAsZeroWorkYears() { |
| | | GuideApplication application = new GuideApplication(); |
| | | application.setMobile("13800138000"); |
| | |
| | | <text class="payment-service">{{ normalizedSelectedServiceTitle }}</text> |
| | | <text class="payment-desc">{{ selectedServiceDuration }}</text> |
| | | </view> |
| | | <text class="payment-unit-price">¥ {{ serviceUnitPrice }}/人</text> |
| | | <text class="payment-unit-price">¥ {{ serviceOriginalUnitPrice }}/人</text> |
| | | </view> |
| | | <view class="payment-row"> |
| | | <text class="payment-label">人数</text> |
| | | <text class="payment-value">{{ selectedServiceQuantity }}人</text> |
| | | <text class="payment-price">¥{{ serviceSubtotalDisplay }}</text> |
| | | <text class="payment-price">¥{{ serviceOriginalSubtotalDisplay }}</text> |
| | | </view> |
| | | <view class="payment-row"> |
| | | <text class="payment-label">平台折扣</text> |
| | |
| | | return Number.isFinite(amount) ? amount.toFixed(2) : '0.00' |
| | | } |
| | | |
| | | const serviceUnitPrice = computed(() => formatMoney(selectedService.value.price)) |
| | | const serviceOriginalUnitPrice = computed(() => formatMoney( |
| | | selectedService.value.originalPrice ?? selectedService.value.price |
| | | )) |
| | | |
| | | const serviceOriginalSubtotal = computed(() => Number( |
| | | selectedService.value.originalPrice ?? selectedService.value.price ?? 0 |
| | |
| | | )) |
| | | |
| | | const payableAmount = computed(() => Number( |
| | | Math.max(0, serviceOriginalSubtotal.value - platformDiscount.value).toFixed(2) |
| | | Math.max(0, serviceSubtotal.value).toFixed(2) |
| | | )) |
| | | |
| | | const serviceSubtotalDisplay = computed(() => formatMoney(serviceSubtotal.value)) |
| | | const serviceOriginalSubtotalDisplay = computed(() => formatMoney(serviceOriginalSubtotal.value)) |
| | | const platformDiscountDisplay = computed(() => formatMoney(platformDiscount.value)) |
| | | const payableAmountDisplay = computed(() => formatMoney(payableAmount.value)) |
| | | |