ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/factory/ChargingOrderFallbackFactory.java
@@ -224,6 +224,11 @@ return R.fail("获取站点集合中总的充电量失败:" + throwable.getMessage()); } @Override public R<List<Long>> getAppUserIdsBySiteIds(List<Integer> siteIds) { return R.fail("查询站点充电用户失败:" + throwable.getMessage()); } // @Override // public R<List<TChargingOrder>> getOrderListByStatus(List<Integer> statusList) { // return R.fail("获取站点集合中总的充电量失败:" + throwable.getMessage()); ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/feignClient/ChargingOrderClient.java
@@ -326,6 +326,12 @@ @GetMapping("/t-charging-order/getSumDegreeBySiteIds") R<BigDecimal> getSumDegreeBySiteIds(@RequestParam("siteIds") List<Integer> SiteIds); /** * 查询指定站点中实际发生过充电的用户ID */ @PostMapping("/t-charging-order/getAppUserIdsBySiteIds") R<List<Long>> getAppUserIdsBySiteIds(@RequestBody List<Integer> siteIds); // /** // * 获取充电订单列表 ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
@@ -38,6 +38,8 @@ private String licensePlate; @ApiModelProperty(value = "手机号") private String phone; @ApiModelProperty(value = "归属合作商/服务商") private String partnerName; @ApiModelProperty(value = "uid") private String uid; @ApiModelProperty(value = "记录按钮权限 ") ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -157,6 +157,8 @@ private TAppUserWhitelistService appUserWhitelistService; @Resource private PartnerClient partnerClient; @Resource private PartnerAppUserAccessService partnerAppUserAccessService; @Autowired private RedissonClient redissonClient; @@ -400,6 +402,10 @@ .eq(userListQueryDto.getVipTypeId() != null, TAppUser::getVipId, userListQueryDto.getVipTypeId()) .in(!userIds.isEmpty(),TAppUser::getId,userIds) .orderByDesc(BasePojo::getCreateTime); List<Long> partnerVisibleUserIds = partnerAppUserAccessService.getVisibleUserIdsForCurrentPartner(); if (partnerVisibleUserIds != null) { query.in(TAppUser::getId, partnerVisibleUserIds); } if (Integer.valueOf(1).equals(userListQueryDto.getWhitelistFlag())) { if (whitelistUserIds.isEmpty() && whitelistPhones.isEmpty()) { query.eq(TAppUser::getId, -1L); @@ -690,6 +696,7 @@ @PostMapping(value = "/user/addOrUpdate") @Log(title = "【用户列表】添加编辑用户", businessType = BusinessType.INSERT,operatorType = OperatorType.MANAGE) public R addOrUpdate(@RequestBody TAppUser tAppUser) { partnerAppUserAccessService.assertWritable(); if (tAppUser.getId() != null){ //为编辑操作,要进行vip的逻辑处理 TAppUser nowUser = appUserService.getById(tAppUser.getId()); @@ -734,10 +741,11 @@ } @RequiresPermissions(value = {"/appUser/select", "/vipList/select"}, logical = Logical.OR) @RequiresPermissions(value = {"/appUser/list", "/appUser/select", "/vipList/select"}, logical = Logical.OR) @ApiOperation(value = "用户详情", tags = {"用户管理-用户列表"}) @GetMapping(value = "/user/detail/{id}") public R<UserDetailDto> userDetail(@PathVariable Long id) { partnerAppUserAccessService.assertUserVisible(id); TAppUser user = appUserService.getById(id); R<Map<Integer, String>> vipMap = otherClient.getVipMap(Collections.singletonList(user.getVipId())); if (user.getVipEndTime()!=null&&user.getVipEndTime().isAfter(LocalDateTime.now())) { @@ -772,13 +780,14 @@ return R.ok(userDetailDto); } @RequiresPermissions(value = {"/appUser/select", "/vipList/select"}, logical = Logical.OR) @RequiresPermissions(value = {"/appUser/list", "/appUser/select", "/vipList/select"}, logical = Logical.OR) @ApiOperation(value = "用户详情积分明细", tags = {"后台-用户管理-用户列表"}) @PostMapping(value = "/user/points/page") public R<Page<TAppUserIntegralChange>> pointsDetail(@RequestBody PointsQueryDto pointsQueryDto) { if (pointsQueryDto.getUserId()==null){ pointsQueryDto.setUserId(tokenService.getLoginUserApplet().getUserId()); } partnerAppUserAccessService.assertUserVisible(pointsQueryDto.getUserId()); List<Integer> types = new ArrayList<>(); if (pointsQueryDto.getType()==null){ types.add(1); @@ -874,10 +883,11 @@ } @RequiresPermissions(value = {"/appUser/select", "/vipList/select"}, logical = Logical.OR) @RequiresPermissions(value = {"/appUser/list", "/appUser/select", "/vipList/select"}, logical = Logical.OR) @ApiOperation(value = "用户详情余额明细", tags = {"后台-用户管理-用户列表"}) @PostMapping(value = "/user/balance/page") public R<Page<TExchangeCodeUseRecord>> adminBalanceDetail(@RequestBody ExchangeCodeUseRecordQuery queryDto) { partnerAppUserAccessService.assertUserVisible(queryDto.getUserId()); return otherClient.exchangeCodeUseRecordPage(queryDto); } @@ -945,6 +955,7 @@ @PostMapping(value = "/user/status/change") @Log(title = "【用户列表】冻结解冻用户", businessType = BusinessType.UPDATE,operatorType = OperatorType.MANAGE) public R pointsDetail(@RequestBody UserChangeDto userChangeDto) { partnerAppUserAccessService.assertWritable(); TAppUser appUser = appUserService.getById(userChangeDto.getUserId()); appUser.setStatus(userChangeDto.getStatus()); appUser.setRemark(userChangeDto.getRemark()); @@ -958,6 +969,7 @@ @PostMapping(value = "/user/unit/change") @Log(title = "【用户列表】修改用户单位", businessType = BusinessType.UPDATE,operatorType = OperatorType.MANAGE) public R unitChange(@RequestBody UnitChangeDto unitChangeDto) { partnerAppUserAccessService.assertWritable(); for (String s : unitChangeDto.getIds().split(",")) { TAppUser byId = appUserService.getById(s); byId.setCompanyId(unitChangeDto.getUnitId()); @@ -972,6 +984,7 @@ @DeleteMapping(value = "/user/delete") @Log(title = "【用户列表】删除用户", businessType = BusinessType.DELETE,operatorType = OperatorType.MANAGE) public R userDelete(String ids) { partnerAppUserAccessService.assertWritable(); String[] split = ids.split(","); for (String s : split) { appUserService.removeById(s); @@ -1151,6 +1164,7 @@ @PostMapping(value = "/user/give/vip") @Log(title = "【用户列表】赠送会员", businessType = BusinessType.OTHER,operatorType = OperatorType.MANAGE) public R giveVip(@RequestBody GiveVipDto giveVipDto) { partnerAppUserAccessService.assertWritable(); String[] split = giveVipDto.getUserIds().split(","); for (String s : split) { TAppUser nowUser = appUserService.getById(s); ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserSiteFavoriteController.java
@@ -81,11 +81,14 @@ @ApiOperation(value = "开启站点降价提醒", tags = {"小程序-收藏"}) public AjaxResult<Void> subscribe(@PathVariable Integer siteId) { Long userId = tokenService.getLoginUserApplet().getUserId(); favoriteService.lambdaUpdate() boolean updated = favoriteService.lambdaUpdate() .eq(TAppUserSiteFavorite::getAppUserId, userId) .eq(TAppUserSiteFavorite::getSiteId, siteId) .set(TAppUserSiteFavorite::getNotifyEnabled, true) .update(); if (!updated) { return AjaxResult.error("收藏记录不存在,请重新收藏后再开启提醒"); } return AjaxResult.success(); } ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java
New file @@ -0,0 +1,130 @@ package com.ruoyi.account.service; import com.ruoyi.chargingPile.api.domain.SiteMenu; import com.ruoyi.chargingPile.api.feignClient.PartnerClient; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.common.security.service.TokenService; import com.ruoyi.common.security.utils.SecurityUtils; import com.ruoyi.order.api.feignClient.ChargingOrderClient; import com.ruoyi.system.api.domain.SysUser; import com.ruoyi.system.api.feignClient.SysUserClient; import com.ruoyi.system.api.model.LoginUser; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * 合作商后台用户数据范围及只读权限控制。 */ @Slf4j @Service public class PartnerAppUserAccessService { private static final Integer PARTNER_ROLE_TYPE = 2; private static final Long IMPOSSIBLE_USER_ID = -1L; private static final String PARTNER_READ_ONLY_MESSAGE = "合作商账号仅支持查看用户信息"; @Resource private TokenService tokenService; @Resource private SysUserClient sysUserClient; @Resource private PartnerClient partnerClient; @Resource private ChargingOrderClient chargingOrderClient; /** * 非合作商返回null,合作商返回允许查看的用户ID;空权限始终返回不可命中的ID。 */ public List<Long> getVisibleUserIdsForCurrentPartner() { SysUser sysUser = getCurrentSysUser(); if (!isPartner(sysUser)) { return null; } if (sysUser.getObjectId() == null) { return impossibleUserIds(); } R<Set<Integer>> siteResult = partnerClient.authSite(sysUser.getObjectId(), SiteMenu.SITE_LIST.getValue()); if (siteResult == null || !R.isSuccess(siteResult) || siteResult.getData() == null) { return impossibleUserIds(); } List<Integer> siteIds = siteResult.getData().stream() .filter(Objects::nonNull) .filter(siteId -> siteId > 0) .distinct() .sorted() .collect(Collectors.toList()); if (siteIds.isEmpty()) { return impossibleUserIds(); } R<List<Long>> userResult = chargingOrderClient.getAppUserIdsBySiteIds(siteIds); if (userResult == null || !R.isSuccess(userResult) || userResult.getData() == null) { log.warn("查询合作商充电用户失败,partnerId={}", sysUser.getObjectId()); return impossibleUserIds(); } List<Long> userIds = userResult.getData().stream() .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); return userIds.isEmpty() ? impossibleUserIds() : userIds; } public boolean isCurrentPartner() { return isPartner(getCurrentSysUser()); } public void assertUserVisible(Long userId) { List<Long> visibleUserIds = getVisibleUserIdsForCurrentPartner(); if (visibleUserIds != null && (userId == null || !visibleUserIds.contains(userId))) { throw new ServiceException("无权查看该用户信息"); } } public void assertWritable() { if (isCurrentPartner()) { throw new ServiceException(PARTNER_READ_ONLY_MESSAGE); } } private boolean isPartner(SysUser sysUser) { return sysUser != null && Objects.equals(PARTNER_ROLE_TYPE, sysUser.getRoleType()); } private List<Long> impossibleUserIds() { return Collections.singletonList(IMPOSSIBLE_USER_ID); } private SysUser getCurrentSysUser() { LoginUser loginUser = null; try { loginUser = tokenService.getLoginUser(); } catch (Exception ignored) { // 尝试从安全上下文继续获取。 } if (loginUser == null) { try { loginUser = SecurityUtils.getLoginUser(); } catch (Exception ignored) { // 当前请求不是管理后台登录上下文。 } } if (loginUser == null) { return null; } if (loginUser.getSysUser() != null && loginUser.getSysUser().getRoleType() != null) { return loginUser.getSysUser(); } if (loginUser.getUserid() == null) { return loginUser.getSysUser(); } R<SysUser> userResult = sysUserClient.getSysUser(loginUser.getUserid()); return userResult != null && userResult.getData() != null ? userResult.getData() : loginUser.getSysUser(); } } ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/task/FavoritePriceDropTask.java
@@ -92,12 +92,16 @@ } TAppUser user = appUserService.getById(favorite.getAppUserId()); if (user == null || !StringUtils.hasText(user.getWxOpenid())) { log.info("收藏站点降价提醒未发送,用户openid为空,userId={}, siteId={}", favorite.getAppUserId(), favorite.getSiteId()); continue; } if (sendSubscribeMessage(user.getWxOpenid(), favorite, price)) { favorite.setNotifyEnabled(false); favorite.setLastNotifiedPrice(price.getCurrentPrice()); favoriteService.updateById(favorite); log.info("收藏站点降价提醒发送成功,userId={}, siteId={}, currentPrice={}", favorite.getAppUserId(), favorite.getSiteId(), price.getCurrentPrice()); } } } catch (InterruptedException e) { @@ -112,16 +116,13 @@ } private boolean sendSubscribeMessage(String openId, TAppUserSiteFavorite favorite, FavoriteSitePriceVO price) { BigDecimal decrease = favorite.getFavoritePrice().subtract(price.getCurrentPrice()); JSONObject body = new JSONObject(); body.put("touser", openId); body.put("template_id", templateId); body.put("page", page + "?siteId=" + favorite.getSiteId()); JSONObject data = new JSONObject(); data.put("thing1", value(limit(price.getSiteName(), 20))); data.put("amount2", value(favorite.getFavoritePrice().setScale(4, BigDecimal.ROUND_HALF_UP).toPlainString())); data.put("amount3", value(price.getCurrentPrice().setScale(4, BigDecimal.ROUND_HALF_UP).toPlainString())); data.put("amount4", value(decrease.setScale(4, BigDecimal.ROUND_HALF_UP).toPlainString())); data.put("amount4", value(formatAmount(price.getCurrentPrice()))); body.put("data", data); String accessToken = new WxAppletTools(wxRestTemplate, wxConfig, redisService).getAccessToken(""); String result = HttpUtils.post(SEND_URL + accessToken, body.toJSONString()); @@ -140,6 +141,10 @@ return item; } private String formatAmount(BigDecimal amount) { return amount.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "元"; } private String limit(String value, int length) { if (!StringUtils.hasText(value) || value.length() <= length) { return value; ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java
New file @@ -0,0 +1,122 @@ package com.ruoyi.account.service; import com.ruoyi.chargingPile.api.domain.SiteMenu; import com.ruoyi.chargingPile.api.feignClient.PartnerClient; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.common.security.service.TokenService; import com.ruoyi.order.api.feignClient.ChargingOrderClient; import com.ruoyi.system.api.domain.SysUser; import com.ruoyi.system.api.feignClient.SysUserClient; import com.ruoyi.system.api.model.LoginUser; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class PartnerAppUserAccessServiceTest { @Mock private TokenService tokenService; @Mock private SysUserClient sysUserClient; @Mock private PartnerClient partnerClient; @Mock private ChargingOrderClient chargingOrderClient; private PartnerAppUserAccessService service; @BeforeEach void setUp() { service = new PartnerAppUserAccessService(); ReflectionTestUtils.setField(service, "tokenService", tokenService); ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient); ReflectionTestUtils.setField(service, "partnerClient", partnerClient); ReflectionTestUtils.setField(service, "chargingOrderClient", chargingOrderClient); } @Test void partnerCanSeeOnlyUsersWhoChargedAtAuthorizedSites() { mockCurrentUser(2, 9); when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue())) .thenReturn(R.ok(new HashSet<>(Arrays.asList(11, 12)))); when(chargingOrderClient.getAppUserIdsBySiteIds(Arrays.asList(11, 12))) .thenReturn(R.ok(Arrays.asList(101L, 102L, 101L))); List<Long> result = service.getVisibleUserIdsForCurrentPartner(); assertEquals(Arrays.asList(101L, 102L), result); verify(chargingOrderClient).getAppUserIdsBySiteIds(Arrays.asList(11, 12)); } @Test void partnerWithoutAuthorizedSitesGetsImpossibleUserId() { mockCurrentUser(2, 9); when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue())) .thenReturn(R.ok(Collections.singleton(-1))); assertEquals(Collections.singletonList(-1L), service.getVisibleUserIdsForCurrentPartner()); verifyNoInteractions(chargingOrderClient); } @Test void orderServiceFailureDoesNotExposeAllUsers() { mockCurrentUser(2, 9); when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue())) .thenReturn(R.ok(Collections.singleton(11))); when(chargingOrderClient.getAppUserIdsBySiteIds(Collections.singletonList(11))) .thenReturn(R.fail("order unavailable")); assertEquals(Collections.singletonList(-1L), service.getVisibleUserIdsForCurrentPartner()); } @Test void platformUserKeepsUnrestrictedScope() { mockCurrentUser(1, null); assertNull(service.getVisibleUserIdsForCurrentPartner()); verifyNoInteractions(partnerClient, chargingOrderClient); } @Test void partnerCannotReadUserOutsideVisibleScope() { mockCurrentUser(2, 9); when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue())) .thenReturn(R.ok(Collections.singleton(11))); when(chargingOrderClient.getAppUserIdsBySiteIds(Collections.singletonList(11))) .thenReturn(R.ok(Collections.singletonList(101L))); assertThrows(ServiceException.class, () -> service.assertUserVisible(999L)); } @Test void partnerUserManagementIsReadOnly() { mockCurrentUser(2, 9); ServiceException exception = assertThrows(ServiceException.class, service::assertWritable); assertEquals("合作商账号仅支持查看用户信息", exception.getMessage()); } private void mockCurrentUser(Integer roleType, Integer objectId) { SysUser sysUser = new SysUser(); sysUser.setRoleType(roleType); sysUser.setObjectId(objectId); LoginUser loginUser = new LoginUser(); loginUser.setUserid(1L); loginUser.setSysUser(sysUser); when(tokenService.getLoginUser()).thenReturn(loginUser); } } ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TAccountingStrategyController.java
@@ -36,6 +36,7 @@ import io.swagger.annotations.ApiOperation; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @@ -173,6 +174,7 @@ @Log(title = "【计费策略】修改计费策略", businessType = BusinessType.UPDATE,operatorType = OperatorType.MANAGE) @ApiOperation(tags = {"后台-计费策略", "管理后台-站点管理"},value = "修改计费策略") @PostMapping(value = "/update") @Transactional(rollbackFor = Exception.class) public AjaxResult<String> update(@RequestBody TAccountingStrategyDTO dto) { String partnerCheck = fillAndCheckPartner(dto); if (partnerCheck != null) { @@ -191,9 +193,7 @@ if (Objects.isNull(original)) { return AjaxResult.error("计费策略不存在"); } if(Objects.isNull(dto.getDiscount())){ accountingStrategyService.update(Wrappers.<TAccountingStrategy>lambdaUpdate().set(TAccountingStrategy::getDiscount, null).eq(TAccountingStrategy::getId, dto.getId())); } boolean clearDiscount = Objects.isNull(dto.getDiscount()); // 历史审核通过的策略在替换旧策略后可能仍保留已删除策略的 parentId。 // 这类正式策略再次编辑会被改为待审核子记录,并被计费策略列表过滤掉,先将其恢复为主策略。 if (original.getParentId() != null && Objects.equals(original.getAuditStatus(), 3) @@ -220,12 +220,14 @@ } else { dto.setId(original.getId()); accountingStrategyService.updateById(dto); clearStrategyDiscountIfNeeded(dto.getId(), clearDiscount); replaceStrategyDetails(dto.getId(), dto.getAccountingStrategyDetails()); } return AjaxResult.success(); } // 判断修改的计费策略是否为已通过或已驳回 if(null != dto.getAuditStatus() && (dto.getAuditStatus() == 3 || dto.getAuditStatus() == 4)){ // 是否为正式/驳回策略必须以数据库原记录为准,不能信任前端回传的审核状态。 // 正式策略修改时仅维护待审核子记录,审核通过前站点仍引用原策略。 if (Objects.equals(original.getAuditStatus(), 3) || Objects.equals(original.getAuditStatus(), 4)) { // 查询是否有下级审核中的策略 TAccountingStrategy children = accountingStrategyService.getOne(Wrappers.lambdaQuery(TAccountingStrategy.class) .eq(TAccountingStrategy::getParentId, dto.getId()) @@ -243,6 +245,7 @@ dto.setFirstAuditTime(null); dto.setTwoAuditTime(null); accountingStrategyService.updateById(dto); clearStrategyDiscountIfNeeded(dto.getId(), clearDiscount); replaceStrategyDetails(children.getId(), dto.getAccountingStrategyDetails()); }else { Long userId = SecurityUtils.getLoginUser().getUserid(); @@ -257,20 +260,20 @@ }else { dto.setAuditStatus(1); accountingStrategyService.updateById(dto); clearStrategyDiscountIfNeeded(dto.getId(), clearDiscount); replaceStrategyDetails(dto.getId(), dto.getAccountingStrategyDetails()); } if(null != dto.getSiteId()){ //下发硬件数据 List<TChargingPile> list = chargingPileService.lambdaQuery().eq(TChargingPile::getSiteId, dto.getSiteId()).eq(TChargingPile::getDelFlag, 0).list(); for (TChargingPile tChargingPile : list) { integrationClient.setupBillingModel1(tChargingPile.getCode(), JSON.toJSONString(dto.getAccountingStrategyDetails())); chengDuJianGuanClient.notificationStationFee(tChargingPile.getId()); } } return AjaxResult.success(); } private void clearStrategyDiscountIfNeeded(Integer strategyId, boolean clearDiscount) { if (clearDiscount) { accountingStrategyService.update(Wrappers.<TAccountingStrategy>lambdaUpdate() .set(TAccountingStrategy::getDiscount, null) .eq(TAccountingStrategy::getId, strategyId)); } } private void replaceStrategyDetails(Integer strategyId, List<TAccountingStrategyDetail> details) { accountingStrategyDetailService.remove(Wrappers.lambdaQuery(TAccountingStrategyDetail.class) .eq(TAccountingStrategyDetail::getAccountingStrategyId, strategyId)); ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -670,6 +670,7 @@ export.setPartnerName(dto.getPartnerName()); export.setPhone(dto.getPhone()); export.setOrderSource(formatOrderSource(dto.getOrderSource())); export.setCreateTime(dto.getCreateTime() == null ? null : DateUtils.localDateTimeToString(dto.getCreateTime())); export.setStartTime(dto.getStartTime() == null ? null : DateUtils.localDateTimeToString(dto.getStartTime())); export.setEndTime(dto.getEndTime() == null ? null : DateUtils.localDateTimeToString(dto.getEndTime())); export.setLicensePlate(dto.getLicensePlate()); @@ -1285,6 +1286,14 @@ return R.ok(count); } /** * 查询指定站点中实际发生过充电的用户ID */ @PostMapping(value = "/getAppUserIdsBySiteIds") public R<List<Long>> getAppUserIdsBySiteIds(@RequestBody List<Integer> siteIds) { return R.ok(chargingOrderService.getAppUserIdsBySiteIds(siteIds)); } //订单详情 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TOrderInvoiceController.java
@@ -85,8 +85,8 @@ @RequiresPermissions(value = {"/invoiceManagement"}, logical = Logical.OR) @PostMapping("/invoicingCompanyOptions") @ApiOperation(value = "查询开票公司选项", tags = {"管理后台-发票管理"}) public AjaxResult<List<String>> invoicingCompanyOptions(@RequestBody TOrderInvoiceQuery query) { return AjaxResult.success(orderInvoiceService.invoicingCompanyOptions(query)); public AjaxResult<List<String>> invoicingCompanyOptions() { return AjaxResult.success(orderInvoiceService.invoicingCompanyOptions()); } ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java
@@ -267,41 +267,59 @@ List<ChargingOrderVO> list = chargingOrderService.chargingOrderList(orderQuery).getRecords(); List<EnterpriseChargingDetailExport> exports = new ArrayList<>(); for (ChargingOrderVO order : list) { EnterpriseChargingDetailExport export = new EnterpriseChargingDetailExport(); export.setCode(order.getCode()); export.setOrderType(Integer.valueOf(1).equals(order.getOrderType()) ? "小程序" : "刷卡"); export.setOrderClassification("充电订单"); export.setSiteName(order.getSiteName()); export.setTerminalName(order.getTerminalName()); export.setStatus(getChargingOrderStatus(order.getStatus())); export.setChargingSecond(order.getChargingSecond()); export.setTotalAmount(order.getOrderAmount()); export.setCouponDiscountAmount(order.getCouponDiscountAmount()); export.setVipDiscountAmount(order.getVipDiscountAmount()); export.setPaymentAmount(order.getPaymentAmount()); export.setRefundAmount(order.getRefundAmount()); export.setPhone(order.getPhone()); export.setOrderSource(getChargingOrderSource(order.getOrderSource())); export.setStartTime(formatExportDateTime(order.getStartTime())); export.setEndTime(formatExportDateTime(order.getEndTime())); exports.add(export); exports.add(buildVipChargingOrderExport(order)); } writeVipChargingOrderWorkbook(exports); } private String getChargingOrderStatus(Integer status) { static EnterpriseChargingDetailExport buildVipChargingOrderExport(ChargingOrderVO order) { EnterpriseChargingDetailExport export = new EnterpriseChargingDetailExport(); export.setCode(order.getCode()); export.setOrderType(Integer.valueOf(1).equals(order.getOrderType()) ? "小程序" : "刷卡"); export.setOrderClassification("充电订单"); export.setSiteName(order.getSiteName()); export.setTerminalName(order.getTerminalName()); export.setStatus(getChargingOrderStatus(order.getStatus())); export.setChargingSecond(order.getChargingSecond()); export.setTotalAmount(order.getOrderAmount()); export.setCouponDiscountAmount(order.getCouponDiscountAmount()); export.setVipDiscountAmount(order.getVipDiscountAmount()); export.setPaymentAmount(order.getPaymentAmount()); export.setRefundAmount(order.getRefundAmount()); export.setPartnerName(order.getPartnerName()); export.setPhone(order.getPhone()); export.setOrderSource(getChargingOrderSource(order.getOrderSource())); export.setCreateTime(formatExportDateTime(order.getCreateTime())); export.setStartTime(formatExportDateTime(order.getStartTime())); export.setEndTime(formatExportDateTime(order.getEndTime())); export.setLicensePlate(order.getLicensePlate()); export.setChargingCapacity(order.getChargingCapacity()); export.setElectrovalence(order.getElectrovalence()); export.setServiceCharge(order.getServiceCharge()); export.setEndMode(getChargingOrderEndMode(order.getEndMode())); export.setPeriodCount(order.getCount()); return export; } private static String getChargingOrderStatus(Integer status) { if (status == null) return "未知"; String[] names = {"未知", "等待中", "启动中", "充电中", "停止中", "已结束"}; return status >= 0 && status < names.length ? names[status] : "未知"; } private String getChargingOrderSource(Integer source) { private static String getChargingOrderSource(Integer source) { if (Integer.valueOf(1).equals(source)) return "明星充电"; if (Integer.valueOf(2).equals(source)) return "快电"; if (Integer.valueOf(3).equals(source)) return "新电途"; return ""; } private static String getChargingOrderEndMode(Integer endMode) { if (endMode == null) return ""; String[] names = {"异常终止", "主动终止", "满电终止", "费用不足终止"}; return endMode >= 0 && endMode < names.length ? names[endMode] : ""; } static String formatExportDateTime(LocalDateTime time) { return time == null ? "" : DateUtils.localDateTimeToString(time); } ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java
@@ -54,6 +54,9 @@ @Excel(name = "订单来源", width = 20) private String orderSource; @Excel(name = "下单时间", width = 30) private String createTime; @Excel(name = "开始时间", width = 30) private String startTime; ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
@@ -209,6 +209,8 @@ List<OrderCountByDate> countOrdersByDate(@Param("startTime")LocalDateTime startTime, @Param("endTime")LocalDateTime endTime,@Param("gunIds")List<Integer> gunIds); Double getSumDegreeBySiteIds(@Param("siteIds") List<Integer> siteIds); List<Long> getAppUserIdsBySiteIds(@Param("siteIds") List<Integer> siteIds); /** ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TOrderInvoiceMapper.java
@@ -33,7 +33,7 @@ /** * 查询开票公司下拉选项 */ List<String> invoicingCompanyOptions(@Param("query") TOrderInvoiceQuery query); List<String> invoicingCompanyOptions(); /** * 查询开票状态数量 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
@@ -393,6 +393,11 @@ R<StatisticsOfBatteryVO> getStatisticsOfBattery(List<Integer> siteIds); Double getSumDegreeBySiteIds(List<Integer> siteIds); /** * 查询指定站点中实际发生过充电的用户ID */ List<Long> getAppUserIdsBySiteIds(List<Integer> siteIds); /** * 获取启动失败且时间超过1小时的订单 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TOrderInvoiceService.java
@@ -56,9 +56,9 @@ PageInfo<TOrderInvoiceVO> pageList(TOrderInvoiceQuery query); /** * 查询当前账号可见的开票公司选项 * 查询全部开票公司选项 */ List<String> invoicingCompanyOptions(TOrderInvoiceQuery query); List<String> invoicingCompanyOptions(); /** * 查询开票状态数量 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingCouponSupport.java
@@ -11,37 +11,72 @@ private ChargingCouponSupport() { } static BigDecimal calculateUpfrontDiscount(TCoupon coupon, BigDecimal rechargeAmount) { if (coupon == null || rechargeAmount == null || rechargeAmount.compareTo(BigDecimal.ZERO) <= 0) { static BigDecimal calculatePrepaymentAmount(BigDecimal rechargeAmount, boolean whitelistAllFree) { if (whitelistAllFree) { return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); } if (!Integer.valueOf(1).equals(coupon.getType())) { throw new IllegalArgumentException("请选择充电优惠券"); return money(rechargeAmount).setScale(2, RoundingMode.HALF_UP); } /** * 兼容曾在预付阶段直接减掉优惠券金额的在途订单,退款基数不能超过真实收款。 */ static BigDecimal resolveSettlementPrepayment(BigDecimal rechargeAmount, BigDecimal balancePayAmount, BigDecimal thirdPayAmount, BigDecimal upfrontCouponDiscount) { BigDecimal recharge = money(rechargeAmount); BigDecimal collected = money(balancePayAmount).add(money(thirdPayAmount)); if (money(upfrontCouponDiscount).compareTo(BigDecimal.ZERO) > 0 && collected.compareTo(BigDecimal.ZERO) > 0 && collected.compareTo(recharge) < 0) { return collected; } BigDecimal threshold = coupon.getMeetTheConditions() == null ? BigDecimal.ZERO : coupon.getMeetTheConditions(); if (rechargeAmount.compareTo(threshold) < 0) { throw new IllegalArgumentException("预付金额未达到优惠券使用门槛"); return recharge; } /** * 优惠券只在充电结束时结算,且优惠金额不能超过剩余服务费。 */ static BigDecimal calculateSettlementDiscount(TCoupon coupon, BigDecimal actualAmountBeforeCoupon, BigDecimal remainingServiceCharge) { BigDecimal actualAmount = money(actualAmountBeforeCoupon); BigDecimal serviceCharge = money(remainingServiceCharge); if (coupon == null || actualAmount.compareTo(BigDecimal.ZERO) <= 0 || serviceCharge.compareTo(BigDecimal.ZERO) <= 0) { return BigDecimal.ZERO.setScale(4, RoundingMode.HALF_EVEN); } BigDecimal threshold = money(coupon.getMeetTheConditions()); if (actualAmount.compareTo(threshold) < 0) { return BigDecimal.ZERO.setScale(4, RoundingMode.HALF_EVEN); } BigDecimal discount; BigDecimal discount = BigDecimal.ZERO; if (Integer.valueOf(1).equals(coupon.getPreferentialMode())) { discount = coupon.getDiscountAmount(); } else if (Integer.valueOf(2).equals(coupon.getPreferentialMode())) { if (coupon.getDiscount() == null) { throw new IllegalArgumentException("优惠券折扣配置无效"); } discount = rechargeAmount.multiply(TEN.subtract(coupon.getDiscount())) .divide(TEN, 2, RoundingMode.HALF_UP); discount = money(coupon.getDiscountAmount()); } else if (Integer.valueOf(2).equals(coupon.getPreferentialMode()) && coupon.getDiscount() != null) { discount = actualAmount.multiply(TEN.subtract(coupon.getDiscount())).divide(TEN); if (coupon.getMaximumDiscountAmount() != null) { discount = discount.min(coupon.getMaximumDiscountAmount()); } } else { throw new IllegalArgumentException("优惠券优惠方式无效"); } if (discount == null || discount.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("优惠券优惠金额无效"); return discount.max(BigDecimal.ZERO).min(serviceCharge).setScale(4, RoundingMode.HALF_EVEN); } static BigDecimal[] calculateWhitelistRefundSplit(boolean whitelistOrder, boolean whitelistAllFree, BigDecimal electrovalence, BigDecimal serviceCharge) { if (!whitelistOrder) { return new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO}; } return discount.min(rechargeAmount).setScale(2, RoundingMode.HALF_UP); BigDecimal electricDiscount = whitelistAllFree ? money(electrovalence) : BigDecimal.ZERO; BigDecimal serviceDiscount = money(serviceCharge); return new BigDecimal[]{electricDiscount, serviceDiscount}; } private static BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } } ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -714,7 +714,7 @@ return chargingOrder != null && "ICBC".equals(chargingOrder.getSourceName()); } private BigDecimal resolveUpfrontCouponDiscount(Long appCouponId, Long userId, BigDecimal rechargeAmount) { private void validateSelectedChargingCoupon(Long appCouponId, Long userId) { TAppCoupon appCoupon = appCouponClient.getAppCouponById(appCouponId).getData(); if (appCoupon == null || !userId.equals(appCoupon.getAppUserId())) { throw new ServiceException("优惠券不存在或不属于当前用户"); @@ -727,11 +727,9 @@ || (appCoupon.getEndTime() != null && appCoupon.getEndTime().isBefore(now))) { throw new ServiceException("优惠券不在有效期内"); } try { TCoupon coupon = JSON.parseObject(appCoupon.getCouponJson(), TCoupon.class); return ChargingCouponSupport.calculateUpfrontDiscount(coupon, rechargeAmount); } catch (IllegalArgumentException e) { throw new ServiceException(e.getMessage()); TCoupon coupon = JSON.parseObject(appCoupon.getCouponJson(), TCoupon.class); if (coupon == null || !Integer.valueOf(1).equals(coupon.getType())) { throw new ServiceException("请选择充电优惠券"); } } @@ -765,20 +763,11 @@ * @return 数组下标0=电费减免,下标1=服务费减免 */ private BigDecimal[] calculateWhitelistRefundSplit(TChargingOrder chargingOrder, BigDecimal electrovalence, BigDecimal serviceCharge) { BigDecimal electricDiscount = BigDecimal.ZERO; BigDecimal serviceDiscount = BigDecimal.ZERO; if (!isWhitelistOrder(chargingOrder)) { return new BigDecimal[]{electricDiscount, serviceDiscount}; } if (Integer.valueOf(WHITELIST_BENEFIT_ALL_FREE).equals(chargingOrder.getWhitelistBenefitType())) { electricDiscount = electrovalence == null ? BigDecimal.ZERO : electrovalence; serviceDiscount = serviceCharge == null ? BigDecimal.ZERO : serviceCharge; } else if (Integer.valueOf(WHITELIST_BENEFIT_ELECTRIC_FREE).equals(chargingOrder.getWhitelistBenefitType())) { electricDiscount = electrovalence == null ? BigDecimal.ZERO : electrovalence; } else if (Integer.valueOf(WHITELIST_BENEFIT_SERVICE_FREE).equals(chargingOrder.getWhitelistBenefitType())) { serviceDiscount = serviceCharge == null ? BigDecimal.ZERO : serviceCharge; } return new BigDecimal[]{electricDiscount, serviceDiscount}; return ChargingCouponSupport.calculateWhitelistRefundSplit( isWhitelistOrder(chargingOrder), isWhitelistAllFreeOrder(chargingOrder), electrovalence, serviceCharge); } private TVip getVipSnapshot(TAppUserVipDetail vipDetail, Integer vipId) { if (vipDetail != null && StringUtils.hasLength(vipDetail.getVipJson())) { @@ -839,7 +828,6 @@ && Integer.valueOf(WHITELIST_BENEFIT_ALL_FREE).equals(whitelist.getBenefitType()); BigDecimal userBalance = money(appUser.getBalance()); BigDecimal balancePayAmount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); BigDecimal upfrontCouponDiscount = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); if (payMethod == 2 && userBalance.compareTo(rechargeAmount) >= 0) { payMethod = 1; addChargingOrder.setPayMethod(1); @@ -847,12 +835,10 @@ if (isBalancePayMode(payMethod)) { addChargingOrder.setAppUserCouponId(null); } else if (!whitelistMatched && addChargingOrder.getAppUserCouponId() != null) { upfrontCouponDiscount = resolveUpfrontCouponDiscount( addChargingOrder.getAppUserCouponId(), userId, rechargeAmount); // 这里只校验优惠券,不抵扣预付金额;实际门槛和优惠金额在充电结束时计算。 validateSelectedChargingCoupon(addChargingOrder.getAppUserCouponId(), userId); } BigDecimal payableAmount = whitelistAllFree ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : rechargeAmount.subtract(upfrontCouponDiscount).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); BigDecimal payableAmount = ChargingCouponSupport.calculatePrepaymentAmount(rechargeAmount, whitelistAllFree); BigDecimal thirdPayAmount = payableAmount; if (isBalancePayMode(payMethod)) { if (payMethod == 1 && userBalance.compareTo(payableAmount) < 0) { @@ -956,7 +942,7 @@ chargingOrder.setThirdRefundAmount(BigDecimal.ZERO); chargingOrder.setRechargeAmount(rechargeAmount); chargingOrder.setAppCouponId(addChargingOrder.getAppUserCouponId()); chargingOrder.setCouponDiscountAmount(upfrontCouponDiscount); chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO); chargingOrder.setIsWhitelist(whitelistMatched ? 1 : 0); chargingOrder.setWhitelistId(whitelistMatched ? whitelist.getWhitelistId() : null); chargingOrder.setWhitelistBenefitType(whitelistMatched ? whitelist.getBenefitType() : null); @@ -4439,8 +4425,11 @@ BigDecimal rechargeAmount = chargingOrder.getRechargeAmount(); //支付金额 BigDecimal payAmount = periodElectricPrice_total.add(serviceCharge_total); //退款金额=充值金额-实际支付金额 BigDecimal refundAmount = rechargeAmount.subtract(payAmount); //退款金额=真实预付金额-实际支付金额;兼容曾在预付阶段错误抵扣优惠券的在途订单。 BigDecimal settlementPrepayment = ChargingCouponSupport.resolveSettlementPrepayment( rechargeAmount, chargingOrder.getBalancePayAmount(), chargingOrder.getThirdPayAmount(), chargingOrder.getCouponDiscountAmount()); BigDecimal refundAmount = settlementPrepayment.subtract(payAmount); UploadRealTimeMonitoringData uploadRealTimeMonitoringData = uploadRealTimeMonitoringDataService.getLastDataById(chargingOrder.getCode()); if (null != uploadRealTimeMonitoringData && null == chargingOrder.getEndMode()) { @@ -4468,57 +4457,21 @@ } //计算优惠券 boolean couponAppliedUpfront = money(chargingOrder.getCouponDiscountAmount()).compareTo(BigDecimal.ZERO) > 0; BigDecimal couponDiscount = couponAppliedUpfront ? money(chargingOrder.getCouponDiscountAmount()) : BigDecimal.ZERO; BigDecimal couponDiscount = BigDecimal.ZERO; if (!isWhitelistOrder(chargingOrder) && !isBalancePayMode(chargingOrder.getPayMethod()) && null != chargingOrder.getAppCouponId()) { Long appCouponId = chargingOrder.getAppCouponId(); TAppCoupon appCoupon = appCouponClient.getAppCouponById(chargingOrder.getAppCouponId()).getData(); String couponJson = appCoupon.getCouponJson(); TCoupon tCoupon = JSON.parseObject(couponJson, TCoupon.class); Integer preferentialMode = tCoupon.getPreferentialMode(); if (couponAppliedUpfront) { couponDiscount = ChargingCouponSupport.calculateSettlementDiscount( tCoupon, payAmount, serviceCharge_total); if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) { appCoupon.setStatus(2); appCouponClient.updateAppCoupon(appCoupon); } else if (1 == preferentialMode) { //满减 if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) { BigDecimal couponDiscountAmount = tCoupon.getDiscountAmount(); //如果优惠金额大于服务费金额,以服务费作为最大限制 if (serviceCharge_total.compareTo(couponDiscountAmount) < 0) { couponDiscount = serviceCharge_total; serviceCharge_total = BigDecimal.ZERO; } else { couponDiscount = couponDiscountAmount; } appCoupon.setStatus(2); appCouponClient.updateAppCoupon(appCoupon); } else { appCouponClient.refund(chargingOrder.getAppCouponId().toString()); chargingOrder.setAppCouponId(null); chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO); } } if (!couponAppliedUpfront && 2 == preferentialMode) { //抵扣 if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) { //折扣金额 BigDecimal divide = payAmount.multiply(new BigDecimal(10).subtract(tCoupon.getDiscount())).divide(new BigDecimal(10)); divide = divide.compareTo(tCoupon.getMaximumDiscountAmount()) > 0 ? tCoupon.getMaximumDiscountAmount() : divide; //如果优惠金额大于服务费金额,以服务费作为最大限制 if (serviceCharge_total.compareTo(divide) < 0) { couponDiscount = serviceCharge_total; serviceCharge_total = BigDecimal.ZERO; } else { couponDiscount = divide; } appCoupon.setStatus(2); appCouponClient.updateAppCoupon(appCoupon); } else { chargingOrder.setAppCouponId(null); chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO); appCouponClient.refund(chargingOrder.getAppCouponId().toString()); } } else { appCouponClient.refund(appCouponId.toString()); chargingOrder.setAppCouponId(null); chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO); } } BigDecimal[] whitelistRefundSplit = calculateWhitelistRefundSplit(chargingOrder, periodElectricPrice_total, serviceCharge_total); @@ -4534,10 +4487,7 @@ } //优惠券优惠金额 couponDiscount = couponDiscount.setScale(4, RoundingMode.HALF_EVEN); //历史订单在结算时计算优惠;新订单已在预付时少收优惠金额,不能再次增加退款。 if (!couponAppliedUpfront) { refundAmount = refundAmount.add(couponDiscount); } refundAmount = refundAmount.add(couponDiscount); //实际支付金额-优惠券 payAmount = payAmount.subtract(couponDiscount).max(BigDecimal.ZERO); serviceCharge_total = serviceCharge_total.subtract(couponDiscount).max(BigDecimal.ZERO); @@ -6614,6 +6564,14 @@ } return this.list(queryWrapper.orderByDesc(TChargingOrder::getCreateTime)); } @Override public List<Long> getAppUserIdsBySiteIds(List<Integer> siteIds) { if (siteIds == null || siteIds.isEmpty()) { return Collections.emptyList(); } return this.baseMapper.getAppUserIdsBySiteIds(siteIds); } @Override public Long countCar(List<String> brands) { @@ -7323,8 +7281,11 @@ BigDecimal rechargeAmount = chargingOrder.getRechargeAmount(); //支付金额 BigDecimal payAmount = periodElectricPrice_total.add(serviceCharge_total); //退款金额=充值金额-实际支付金额 BigDecimal refundAmount = rechargeAmount.subtract(payAmount); //退款金额=真实预付金额-实际支付金额;兼容曾在预付阶段错误抵扣优惠券的在途订单。 BigDecimal settlementPrepayment = ChargingCouponSupport.resolveSettlementPrepayment( rechargeAmount, chargingOrder.getBalancePayAmount(), chargingOrder.getThirdPayAmount(), chargingOrder.getCouponDiscountAmount()); BigDecimal refundAmount = settlementPrepayment.subtract(payAmount); UploadRealTimeMonitoringData uploadRealTimeMonitoringData = uploadRealTimeMonitoringDataService.getLastDataById(chargingOrder.getCode()); if (null != uploadRealTimeMonitoringData && null == chargingOrder.getEndMode()) { @@ -7352,64 +7313,26 @@ } //计算优惠券 boolean couponAppliedUpfront = money(chargingOrder.getCouponDiscountAmount()).compareTo(BigDecimal.ZERO) > 0; BigDecimal couponDiscount = couponAppliedUpfront ? money(chargingOrder.getCouponDiscountAmount()) : BigDecimal.ZERO; BigDecimal couponDiscount = BigDecimal.ZERO; if (null != chargingOrder.getAppCouponId()) { Long appCouponId = chargingOrder.getAppCouponId(); TAppCoupon appCoupon = appCouponClient.getAppCouponById(chargingOrder.getAppCouponId()).getData(); String couponJson = appCoupon.getCouponJson(); TCoupon tCoupon = JSON.parseObject(couponJson, TCoupon.class); Integer preferentialMode = tCoupon.getPreferentialMode(); if (couponAppliedUpfront) { couponDiscount = ChargingCouponSupport.calculateSettlementDiscount( tCoupon, payAmount, serviceCharge_total); if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) { appCoupon.setStatus(2); appCouponClient.updateAppCoupon(appCoupon); } else if (1 == preferentialMode) { //满减 if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) { BigDecimal couponDiscountAmount = tCoupon.getDiscountAmount(); //如果优惠金额大于服务费金额,以服务费作为最大限制 if (serviceCharge_total.compareTo(couponDiscountAmount) < 0) { couponDiscount = serviceCharge_total; serviceCharge_total = BigDecimal.ZERO; } else { couponDiscount = couponDiscountAmount; } appCoupon.setStatus(2); appCouponClient.updateAppCoupon(appCoupon); } else { appCouponClient.refund(chargingOrder.getAppCouponId().toString()); chargingOrder.setAppCouponId(null); chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO); } } if (!couponAppliedUpfront && 2 == preferentialMode) { //抵扣 if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) { //折扣金额 BigDecimal divide = payAmount.multiply(new BigDecimal(10).subtract(tCoupon.getDiscount())).divide(new BigDecimal(10)); divide = divide.compareTo(tCoupon.getMaximumDiscountAmount()) > 0 ? tCoupon.getMaximumDiscountAmount() : divide; //如果优惠金额大于服务费金额,以服务费作为最大限制 if (serviceCharge_total.compareTo(divide) < 0) { couponDiscount = serviceCharge_total; serviceCharge_total = BigDecimal.ZERO; } else { couponDiscount = divide; } appCoupon.setStatus(2); appCouponClient.updateAppCoupon(appCoupon); } else { chargingOrder.setAppCouponId(null); chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO); appCouponClient.refund(chargingOrder.getAppCouponId().toString()); } } else { appCouponClient.refund(appCouponId.toString()); chargingOrder.setAppCouponId(null); chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO); } } //优惠券优惠金额 couponDiscount = couponDiscount.setScale(4, RoundingMode.HALF_EVEN); if (!couponAppliedUpfront) { refundAmount = refundAmount.add(couponDiscount); } refundAmount = refundAmount.add(couponDiscount); //实际支付金额-优惠券 payAmount = payAmount.subtract(couponDiscount).max(BigDecimal.ZERO); serviceCharge_total = serviceCharge_total.subtract(couponDiscount).max(BigDecimal.ZERO); @@ -7550,8 +7473,11 @@ BigDecimal rechargeAmount = chargingOrder.getRechargeAmount(); //支付金额 BigDecimal payAmount = periodElectricPrice_total.add(serviceCharge_total); //退款金额=充值金额-实际支付金额 BigDecimal refundAmount = rechargeAmount.subtract(payAmount); //退款金额=真实预付金额-实际支付金额;兼容曾在预付阶段错误抵扣优惠券的在途订单。 BigDecimal settlementPrepayment = ChargingCouponSupport.resolveSettlementPrepayment( rechargeAmount, chargingOrder.getBalancePayAmount(), chargingOrder.getThirdPayAmount(), chargingOrder.getCouponDiscountAmount()); BigDecimal refundAmount = settlementPrepayment.subtract(payAmount); UploadRealTimeMonitoringData uploadRealTimeMonitoringData = uploadRealTimeMonitoringDataService.getLastDataById(chargingOrder.getCode()); if (null != uploadRealTimeMonitoringData && null == chargingOrder.getEndMode()) { @@ -7581,51 +7507,19 @@ //计算优惠券 BigDecimal couponDiscount = BigDecimal.ZERO; if (null != chargingOrder.getAppCouponId()) { //判断实际充电金额是否满足优惠券使用条件,如果不满足则不适用优惠券。 Long appCouponId = chargingOrder.getAppCouponId(); TAppCoupon appCoupon = appCouponClient.getAppCouponById(chargingOrder.getAppCouponId()).getData(); String couponJson = appCoupon.getCouponJson(); TCoupon tCoupon = com.alibaba.fastjson2.JSON.parseObject(couponJson, TCoupon.class); Integer preferentialMode = tCoupon.getPreferentialMode(); if (1 == preferentialMode) { //满减 if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) { BigDecimal couponDiscountAmount = tCoupon.getDiscountAmount(); //如果优惠金额大于服务费金额,以服务费作为最大限制 if (serviceCharge_total.compareTo(couponDiscountAmount) < 0) { couponDiscount = serviceCharge_total; serviceCharge_total = BigDecimal.ZERO; } else { couponDiscount = couponDiscountAmount; } appCoupon.setStatus(2); appCouponClient.updateAppCoupon(appCoupon); } else { appCouponClient.refund(chargingOrder.getAppCouponId().toString()); chargingOrder.setAppCouponId(null); chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO); } } if (2 == preferentialMode) { //抵扣 if (payAmount.compareTo(tCoupon.getMeetTheConditions()) >= 0) { //折扣金额 BigDecimal divide = payAmount.multiply(new BigDecimal(10).subtract(tCoupon.getDiscount())).divide(new BigDecimal(10)); divide = divide.compareTo(tCoupon.getMaximumDiscountAmount()) > 0 ? tCoupon.getMaximumDiscountAmount() : divide; //如果优惠金额大于服务费金额,以服务费作为最大限制 if (serviceCharge_total.compareTo(divide) < 0) { couponDiscount = serviceCharge_total; serviceCharge_total = BigDecimal.ZERO; } else { couponDiscount = divide; } appCoupon.setStatus(2); appCouponClient.updateAppCoupon(appCoupon); } else { chargingOrder.setAppCouponId(null); chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO); appCouponClient.refund(chargingOrder.getAppCouponId().toString()); } couponDiscount = ChargingCouponSupport.calculateSettlementDiscount( tCoupon, payAmount, serviceCharge_total); if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) { appCoupon.setStatus(2); appCouponClient.updateAppCoupon(appCoupon); } else { appCouponClient.refund(appCouponId.toString()); chargingOrder.setAppCouponId(null); chargingOrder.setCouponDiscountAmount(BigDecimal.ZERO); } } //优惠券优惠金额 @@ -7633,10 +7527,8 @@ //退款金额+优惠券 refundAmount = refundAmount.add(couponDiscount); //实际支付金额-优惠券 payAmount = payAmount.subtract(couponDiscount); if (serviceCharge_total.compareTo(BigDecimal.ZERO) > 0) { serviceCharge_total = serviceCharge_total.subtract(couponDiscount); } payAmount = payAmount.subtract(couponDiscount).max(BigDecimal.ZERO); serviceCharge_total = serviceCharge_total.subtract(couponDiscount).max(BigDecimal.ZERO); chargingOrder.setCouponDiscountAmount(couponDiscount.setScale(2, RoundingMode.HALF_EVEN)); chargingOrder.setServiceCharge(serviceCharge_total.setScale(2, RoundingMode.HALF_EVEN)); chargingOrder.setPaymentAmount(payAmount.setScale(2, RoundingMode.HALF_EVEN)); ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java
@@ -452,12 +452,8 @@ } @Override public List<String> invoicingCompanyOptions(TOrderInvoiceQuery query) { if (query == null) { query = new TOrderInvoiceQuery(); } applyPartnerSiteScope(query); return this.baseMapper.invoicingCompanyOptions(query); public List<String> invoicingCompanyOptions() { return this.baseMapper.invoicingCompanyOptions(); } @Override ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
@@ -2,6 +2,19 @@ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.order.mapper.TChargingOrderMapper"> <select id="getAppUserIdsBySiteIds" resultType="java.lang.Long"> SELECT app_user_id FROM t_charging_order WHERE del_flag = 0 AND app_user_id IS NOT NULL AND status IN (3, 4, 5) AND site_id IN <foreach collection="siteIds" item="siteId" open="(" separator="," close=")"> #{siteId} </foreach> GROUP BY app_user_id </select> <!-- 通用查询映射结果 --> <resultMap id="BaseResultMap" type="com.ruoyi.order.api.model.TChargingOrder"> <id column="id" property="id"/> @@ -92,11 +105,13 @@ <select id="chargingOrder" resultType="com.ruoyi.order.api.vo.ChargingOrderVO"> select t1.*, app_user.phone, partner.name as partnerName, IFNULL(t1.whitelist_electric_refund_amount, 0) + IFNULL(t1.whitelist_service_refund_amount, 0) as whitelistRefundAmount from t_charging_order t1 <if test="req.phone != null and req.phone != ''"> inner join `charging_pile_account`.`t_app_user` app_user on app_user.id = t1.app_user_id </if> left join `charging_pile_account`.`t_app_user` app_user on app_user.id = t1.app_user_id left join `charging_pile_service`.`t_site` site on site.id = t1.site_id left join `charging_pile_service`.`t_partner` partner on partner.id = site.partner_id where t1.del_flag = 0 <if test="null != req.code and req.code!=''"> and t1.code LIKE CONCAT('%',#{req.code},'%') @@ -2630,7 +2645,7 @@ <sql id="enterpriseChargingBaseWhere"> toi.invoicing_object_type = 2 AND toi.status IN (1, 2, 3) AND toi.status IN (2, 3) AND tod.order_type = 1 <if test="data.exactCompanyName != null and data.exactCompanyName != ''"> AND toi.name = #{data.exactCompanyName} ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml
@@ -100,21 +100,8 @@ <select id="invoicingCompanyOptions" resultType="java.lang.String"> SELECT DISTINCT TRIM(toi.invoicing_company) FROM t_order_invoice toi <where> toi.invoicing_company IS NOT NULL AND TRIM(toi.invoicing_company) != '' <if test="query.siteIds != null and query.siteIds.size() > 0"> AND toi.site_id IN <foreach collection="query.siteIds" close=")" open="(" item="siteId" separator=","> #{siteId} </foreach> </if> <if test="query.directOnly != null and query.directOnly == 1"> AND toi.site_id IN ( SELECT id FROM charging_pile_service.t_site WHERE business_category = 1 ) </if> </where> WHERE toi.invoicing_company IS NOT NULL AND TRIM(toi.invoicing_company) != '' ORDER BY TRIM(toi.invoicing_company) </select> <select id="statusCount" resultType="java.util.Map"> ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TChargingOrderControllerTest.java
@@ -12,6 +12,7 @@ import org.springframework.test.util.ReflectionTestUtils; import java.time.LocalDate; import java.time.LocalDateTime; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; @@ -27,9 +28,27 @@ class TChargingOrderControllerTest { @Test void appUserIdsBySiteIdsDelegatesToOrderService() { TChargingOrderService chargingOrderService = mock(TChargingOrderService.class); TChargingOrderController controller = new TChargingOrderController(); ReflectionTestUtils.setField(controller, "chargingOrderService", chargingOrderService); List<Integer> siteIds = Arrays.asList(11, 12); List<Long> userIds = Arrays.asList(101L, 102L); when(chargingOrderService.getAppUserIdsBySiteIds(siteIds)).thenReturn(userIds); com.ruoyi.common.core.domain.R<List<Long>> result = controller.getAppUserIdsBySiteIds(siteIds); assertEquals(userIds, result.getData()); verify(chargingOrderService).getAppUserIdsBySiteIds(siteIds); } @Test void enterpriseChargingDetailExportContainsVisibleDetailColumns() { EnterpriseChargingDetailVO detail = new EnterpriseChargingDetailVO(); detail.setLicensePlate("川JD20960"); detail.setPartnerName("测试合作商"); detail.setOrderSource(0); detail.setCreateTime(LocalDateTime.of(2026, 7, 29, 10, 20, 30)); detail.setChargingCapacity(new BigDecimal("40.693")); detail.setElectrovalence(new BigDecimal("43.0857")); detail.setServiceCharge(new BigDecimal("16.2772")); @@ -40,6 +59,9 @@ new TChargingOrderController(), "buildEnterpriseChargingDetailExport", detail); assertEquals("川JD20960", export.getLicensePlate()); assertEquals("测试合作商", export.getPartnerName()); assertEquals("自有平台", export.getOrderSource()); assertEquals("2026-07-29 10:20:30", export.getCreateTime()); assertEquals(new BigDecimal("40.693"), export.getChargingCapacity()); assertEquals(new BigDecimal("43.0857"), export.getElectrovalence()); assertEquals(new BigDecimal("16.2772"), export.getServiceCharge()); ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/controller/TVipOrderControllerTest.java
@@ -1,7 +1,10 @@ package com.ruoyi.order.controller; import com.ruoyi.order.api.vo.ChargingOrderVO; import com.ruoyi.order.export.EnterpriseChargingDetailExport; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -19,4 +22,32 @@ assertEquals("2026-07-28 09:08:07", TVipOrderController.formatExportDateTime(time)); } @Test void fillsVipChargingOrderExportDetailFields() { ChargingOrderVO order = new ChargingOrderVO(); order.setPartnerName("测试合作商"); order.setPhone("13800000000"); order.setOrderSource(1); order.setCreateTime(LocalDateTime.of(2026, 7, 29, 10, 20, 30)); order.setLicensePlate("川A12345"); order.setChargingCapacity(new BigDecimal("12.3456")); order.setElectrovalence(new BigDecimal("8.12")); order.setServiceCharge(new BigDecimal("3.45")); order.setEndMode(2); order.setCount(4); EnterpriseChargingDetailExport export = TVipOrderController.buildVipChargingOrderExport(order); assertEquals("测试合作商", export.getPartnerName()); assertEquals("13800000000", export.getPhone()); assertEquals("明星充电", export.getOrderSource()); assertEquals("2026-07-29 10:20:30", export.getCreateTime()); assertEquals("川A12345", export.getLicensePlate()); assertEquals(new BigDecimal("12.3456"), export.getChargingCapacity()); assertEquals(new BigDecimal("8.12"), export.getElectrovalence()); assertEquals(new BigDecimal("3.45"), export.getServiceCharge()); assertEquals("满电终止", export.getEndMode()); assertEquals(4, export.getPeriodCount()); } } ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingAnalysisMapperTest.java
@@ -35,6 +35,8 @@ assertFalse(baseWhere.contains("co.del_flag")); assertFalse(baseWhere.contains("co.recharge_payment_status")); assertTrue(baseWhere.contains("AND toi.status IN (2, 3)")); assertFalse(baseWhere.contains("AND toi.status IN (1, 2, 3)")); assertTrue(analysis.contains("LEFT JOIN t_charging_order co ON co.id = tod.order_id")); assertTrue(analysis.contains("COUNT(1) AS invoiceCount")); assertTrue(analysis.contains("detailInvoiceAmount")); ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/TOrderInvoiceCompanyOptionsMapperTest.java
New file @@ -0,0 +1,37 @@ package com.ruoyi.order.mapper; import org.junit.jupiter.api.Test; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; class TOrderInvoiceCompanyOptionsMapperTest { @Test void invoicingCompanyOptionsQueriesAllNonBlankCompanies() throws IOException { InputStream input = getClass().getClassLoader() .getResourceAsStream("mapper/order/TOrderInvoiceMapper.xml"); assertNotNull(input); String mapper; try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { mapper = reader.lines().collect(Collectors.joining("\n")); } int start = mapper.indexOf("<select id=\"invoicingCompanyOptions\""); int end = mapper.indexOf("</select>", start); String statement = mapper.substring(start, end); assertTrue(statement.contains("SELECT DISTINCT TRIM(toi.invoicing_company)")); assertTrue(statement.contains("toi.invoicing_company IS NOT NULL")); assertFalse(statement.contains("query.")); assertFalse(statement.contains("site_id")); } } ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VipChargingOrderExportMapperTest.java
New file @@ -0,0 +1,36 @@ package com.ruoyi.order.mapper; import org.junit.jupiter.api.Test; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; class VipChargingOrderExportMapperTest { @Test void chargingOrderQueryIncludesUserPhoneAndPartnerName() throws IOException { InputStream input = getClass().getClassLoader() .getResourceAsStream("mapper/order/TChargingOrderMapper.xml"); assertNotNull(input); String mapper; try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { mapper = reader.lines().collect(Collectors.joining("\n")); } String statement = mapper.substring( mapper.indexOf("<select id=\"chargingOrder\""), mapper.indexOf("<select id=\"payOrderQuery\"")); assertTrue(statement.contains("app_user.phone")); assertTrue(statement.contains("partner.name as partnerName")); assertTrue(statement.contains("left join `charging_pile_account`.`t_app_user`")); assertTrue(statement.contains("left join `charging_pile_service`.`t_partner`")); } } ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingCouponSupportTest.java
@@ -6,44 +6,81 @@ import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class ChargingCouponSupportTest { @Test void fullReductionDiscountsThePrepaymentBeforeThirdPartyPay() { void couponDoesNotReducePrepayment() { TCoupon coupon = chargingCoupon(1); coupon.setMeetTheConditions(new BigDecimal("3")); coupon.setDiscountAmount(new BigDecimal("1")); coupon.setMeetTheConditions(new BigDecimal("30")); coupon.setDiscountAmount(new BigDecimal("10")); BigDecimal discount = ChargingCouponSupport.calculateUpfrontDiscount(coupon, new BigDecimal("3")); BigDecimal paymentAmount = ChargingCouponSupport.calculatePrepaymentAmount(new BigDecimal("30"), false); assertEquals(new BigDecimal("1.00"), discount); assertEquals(new BigDecimal("2.00"), new BigDecimal("3").subtract(discount)); assertEquals(new BigDecimal("30.00"), paymentAmount); } @Test void percentageDiscountHonorsMaximumDiscount() { void legacyUpfrontDiscountOrderUsesActuallyCollectedAmountAsRefundBase() { BigDecimal prepayment = ChargingCouponSupport.resolveSettlementPrepayment( new BigDecimal("30"), BigDecimal.ZERO, new BigDecimal("20"), new BigDecimal("10")); assertEquals(new BigDecimal("20"), prepayment); } @Test void fullReductionCannotDiscountElectricity() { TCoupon coupon = chargingCoupon(1); coupon.setMeetTheConditions(new BigDecimal("10")); coupon.setDiscountAmount(new BigDecimal("10")); BigDecimal discount = ChargingCouponSupport.calculateSettlementDiscount( coupon, new BigDecimal("15"), new BigDecimal("3")); assertEquals(new BigDecimal("3.0000"), discount); } @Test void percentageDiscountHonorsMaximumDiscountAndServiceCharge() { TCoupon coupon = chargingCoupon(2); coupon.setMeetTheConditions(new BigDecimal("10")); coupon.setDiscount(new BigDecimal("8")); coupon.setMaximumDiscountAmount(new BigDecimal("1.50")); BigDecimal discount = ChargingCouponSupport.calculateUpfrontDiscount(coupon, new BigDecimal("10")); BigDecimal discount = ChargingCouponSupport.calculateSettlementDiscount( coupon, new BigDecimal("10"), new BigDecimal("5")); assertEquals(new BigDecimal("1.50"), discount); assertEquals(new BigDecimal("1.5000"), discount); } @Test void rejectsPrepaymentBelowCouponThreshold() { void couponBelowActualSettlementThresholdIsNotUsed() { TCoupon coupon = chargingCoupon(1); coupon.setMeetTheConditions(new BigDecimal("3")); coupon.setMeetTheConditions(new BigDecimal("30")); coupon.setDiscountAmount(new BigDecimal("1")); IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> ChargingCouponSupport.calculateUpfrontDiscount(coupon, new BigDecimal("2.99"))); BigDecimal discount = ChargingCouponSupport.calculateSettlementDiscount( coupon, new BigDecimal("29.99"), new BigDecimal("5")); assertEquals("预付金额未达到优惠券使用门槛", error.getMessage()); assertEquals(new BigDecimal("0.0000"), discount); } @Test void nonFullWhitelistOnlyWaivesServiceCharge() { BigDecimal[] split = ChargingCouponSupport.calculateWhitelistRefundSplit( true, false, new BigDecimal("10"), new BigDecimal("5")); assertEquals(BigDecimal.ZERO, split[0]); assertEquals(new BigDecimal("5"), split[1]); } @Test void fullWhitelistWaivesElectricityAndServiceCharge() { BigDecimal[] split = ChargingCouponSupport.calculateWhitelistRefundSplit( true, true, new BigDecimal("10"), new BigDecimal("5")); assertEquals(new BigDecimal("10"), split[0]); assertEquals(new BigDecimal("5"), split[1]); } private TCoupon chargingCoupon(int preferentialMode) {