liujie
2026-07-21 5bc349b0321d6058ea859780de6d1bd5a0ffdb35
Merge branch '2.2' of http://120.76.84.145:10101/gitblit/r/java/mx_charging_pile into 2.2
2个文件已添加
21个文件已修改
631 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistImportDTO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrderRefund.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/query/TOrderInvoiceQuery.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysFaqMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysFaqServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysFaqMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserSiteFavoriteController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/task/FavoritePriceDropTask.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/ChargingRefundDto.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/EnterpriseChargingDetailVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingDetailMapperTest.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/PayOrderQueryPerformanceMapperTest.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260721_builtin_partner_role_permissions.sql 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistImportDTO.java
@@ -12,15 +12,15 @@
@ApiModel(value = "AppUserWhitelistImportDTO", description = "白名单用户导入行")
public class AppUserWhitelistImportDTO {
    @Excel(name = "用户手机号", prompt = "填写11位手机号,如:13800138000")
    @Excel(name = "用户手机号", width = 18, prompt = "填写11位手机号,如:13800138000")
    @ApiModelProperty(value = "用户手机号")
    private String phone;
    @Excel(name = "享受权益", prompt = "1=全部免费,2=免电费,3=免服务费", combo = {"1", "2", "3"})
    @Excel(name = "享受权益", width = 18, prompt = "1=全部免费,2=免电费,3=免服务费", combo = {"1", "2", "3"})
    @ApiModelProperty(value = "享受权益")
    private String benefitType;
    @Excel(name = "白名单电站", prompt = "填写all表示全部电站;指定电站填写平台站点编号,多个用英文逗号分隔,如:ST001,ST002")
    @Excel(name = "白名单电站", width = 40, prompt = "填写all表示全部电站;指定电站填写平台站点编号,多个用英文逗号分隔,如:ST001,ST002")
    @ApiModelProperty(value = "白名单电站")
    private String whitelistSite;
}
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrderRefund.java
@@ -125,4 +125,6 @@
    private String userId;
    @TableField(exist = false)
    private String uid;
    @TableField(exist = false)
    private Integer siteId;
}
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/query/TOrderInvoiceQuery.java
@@ -47,6 +47,9 @@
    @ApiModelProperty(value = "站点id")
    private Integer siteId;
    @ApiModelProperty(value = "当前账号可查看的站点id,前端忽略")
    private List<Integer> siteIds;
    @ApiModelProperty(value = "合作商id")
    private Integer partnerId;
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysFaqMapper.java
@@ -10,6 +10,8 @@
 * @date 2026-06-25
 */
public interface SysFaqMapper {
    int ensureFaultPullTutorial();
    /**
     * 查询常见问题
     *
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysFaqServiceImpl.java
@@ -2,6 +2,7 @@
import java.util.List;
import javax.annotation.Resource;
import com.ruoyi.common.core.exception.ServiceException;
import org.springframework.stereotype.Service;
import com.ruoyi.system.mapper.SysFaqMapper;
import com.ruoyi.system.domain.SysFaq;
@@ -15,6 +16,8 @@
 */
@Service
public class SysFaqServiceImpl implements ISysFaqService {
    private static final String FAULT_PULL_TUTORIAL = "故障拔枪教程";
    @Resource
    private SysFaqMapper sysFaqMapper;
@@ -37,6 +40,7 @@
     */
    @Override
    public List<SysFaq> selectSysFaqList(SysFaq sysFaq) {
        sysFaqMapper.ensureFaultPullTutorial();
        return sysFaqMapper.selectSysFaqList(sysFaq);
    }
@@ -70,6 +74,12 @@
     */
    @Override
    public int deleteSysFaqByIds(Long[] faqIds) {
        for (Long faqId : faqIds) {
            SysFaq faq = sysFaqMapper.selectSysFaqById(faqId);
            if (faq != null && FAULT_PULL_TUTORIAL.equals(faq.getQuestion())) {
                throw new ServiceException("故障拔枪教程为固定内容,只能编辑");
            }
        }
        return sysFaqMapper.deleteSysFaqByIds(faqIds);
    }
@@ -81,6 +91,10 @@
     */
    @Override
    public int deleteSysFaqById(Long faqId) {
        SysFaq faq = sysFaqMapper.selectSysFaqById(faqId);
        if (faq != null && FAULT_PULL_TUTORIAL.equals(faq.getQuestion())) {
            throw new ServiceException("故障拔枪教程为固定内容,只能编辑");
        }
        return sysFaqMapper.deleteSysFaqById(faqId);
    }
ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysFaqMapper.xml
@@ -23,6 +23,14 @@
        from sys_faq
    </sql>
    <insert id="ensureFaultPullTutorial">
        insert into sys_faq (question, answer, sort_order, status, create_by, create_time, del_flag)
        select '故障拔枪教程', '', 0, '0', 'system', now(), '0'
        where not exists (
            select 1 from sys_faq where question = '故障拔枪教程' and del_flag = '0'
        )
    </insert>
    <select id="selectSysFaqList" parameterType="com.ruoyi.system.domain.SysFaq" resultMap="SysFaqResult">
        <include refid="selectSysFaqVo"/>
        <where>
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserSiteFavoriteController.java
@@ -31,7 +31,7 @@
    @Resource
    private SiteClient siteClient;
    @Value("${wx.applet.price-drop-template-id:}")
    @Value("${wx.applet.price-drop-template-id:7nCycRUZxQ2jxAb2UaYazjp2sPZy3qYyeLqVytyhHhY}")
    private String priceDropTemplateId;
    @GetMapping("/list")
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/task/FavoritePriceDropTask.java
@@ -53,10 +53,10 @@
    @Resource
    private RedissonClient redissonClient;
    @Value("${wx.applet.price-drop-template-id:}")
    @Value("${wx.applet.price-drop-template-id:7nCycRUZxQ2jxAb2UaYazjp2sPZy3qYyeLqVytyhHhY}")
    private String templateId;
    @Value("${wx.applet.price-drop-page:pagesMine/myCollection/myCollection}")
    @Value("${wx.applet.price-drop-page:pagesMine/favoriteSite/favoriteSite}")
    private String page;
    @Scheduled(fixedDelay = 30 * 60 * 1000L, initialDelay = 2 * 60 * 1000L)
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java
@@ -270,25 +270,28 @@
    }
    Integer resolveDefaultRoleId(Integer partnerType) {
        String roleName = partnerTypeRoleName(partnerType);
        Integer roleId = this.baseMapper.getDefaultPartnerRoleId(roleName);
        if (roleId == null && !Integer.valueOf(1).equals(partnerType)) {
            roleId = this.baseMapper.getDefaultPartnerRoleId("合作商");
        List<String> roleNames = partnerTypeRoleNames(partnerType);
        Integer roleId = null;
        for (String roleName : roleNames) {
            roleId = this.baseMapper.getDefaultPartnerRoleId(roleName);
            if (roleId != null) {
                break;
            }
        }
        if (roleId == null) {
            throw new RuntimeException("系统内置角色【" + roleName + "】不存在,请先配置角色");
            throw new RuntimeException("系统内置角色【" + roleNames.get(0) + "】不存在,请先配置角色");
        }
        return roleId;
    }
    private String partnerTypeRoleName(Integer partnerType) {
    private List<String> partnerTypeRoleNames(Integer partnerType) {
        if (Integer.valueOf(2).equals(partnerType)) {
            return "场地方";
            return Arrays.asList("场地方", "合作商");
        }
        if (Integer.valueOf(3).equals(partnerType)) {
            return "服务商";
            return Arrays.asList("服务商", "合作商");
        }
        return "合作商";
        return Arrays.asList("独立合作商", "合作商");
    }
    /**
     * 删除合作商
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImplTest.java
@@ -60,6 +60,18 @@
    }
    @Test
    void independentPartnerUsesUnifiedRoleNameBeforeLegacyAlias() {
        PartnerServiceImpl service = new PartnerServiceImpl();
        PartnerMapper mapper = mock(PartnerMapper.class);
        ReflectionTestUtils.setField(service, "baseMapper", mapper);
        when(mapper.getDefaultPartnerRoleId("独立合作商")).thenReturn(11);
        assertEquals(11, service.resolveDefaultRoleId(1));
        verify(mapper).getDefaultPartnerRoleId("独立合作商");
        verify(mapper, never()).getDefaultPartnerRoleId("合作商");
    }
    @Test
    void clearingShareRatiosStillProducesExplicitDatabaseUpdate() {
        PartnerServiceImpl service = new PartnerServiceImpl();
        PartnerMapper mapper = mock(PartnerMapper.class);
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -366,7 +366,7 @@
        export.setPhone(dto.getPhone());
        export.setOrderStatus(dto.getPayTime() != null ? "订单支付成功" : "订单完成");
        export.setStatus(formatPayOrderStatus(dto.getType(), dto.getStatus()));
        export.setRefundStatus(dto.getRefundStatus() != null && dto.getRefundStatus() == 1 ? "有退款" : "无退款");
        export.setRefundStatus(dto.getRefundStatus() != null && dto.getRefundStatus() > 0 ? "有退款" : "无退款");
        export.setRechargeAmount(dto.getRechargeAmount());
        export.setPaymentAmount(dto.getPaymentAmount());
        export.setFinalAmount(dto.getFinalAmount());
@@ -623,54 +623,19 @@
    @PostMapping(value = "/pay/order/refund/list")
    @ApiOperation(value = "列表", tags = {"管理后台-支付订单-退款订单"})
    public R<PageInfo<TChargingOrderRefund>> refundList(@RequestBody ChargingRefundDto chargingRefundDto) {
        Integer pageCurr = chargingRefundDto.getPageCurr();
        Integer pageSize = chargingRefundDto.getPageSize();
//        R<PageInfo<TChargingOrderRefund>> refundList = chargingOrderService.getRefundList(chargingRefundDto);
        chargingRefundDto.setPageCurr(1);
        chargingRefundDto.setPageSize(199999999);
        R<PageInfo<TChargingOrderRefund>> refundList1 = chargingOrderService.getRefundList(chargingRefundDto);
        Long userId = tokenService.getLoginUser().getUserid();
        //如果没传siteId,获取当前登陆人所有的siteIds
        List<Integer> siteIds = new ArrayList<>();
            if (userId != null){
                List<GetSiteListDTO> data = siteClient.getSiteListByUserId(userId).getData();
                for (GetSiteListDTO datum : data) {
                    siteIds.add(datum.getId());
        SysUser sysUser = sysUserClient.getSysUser(userId).getData();
        if (sysUser != null && Integer.valueOf(2).equals(sysUser.getRoleType())) {
            List<Integer> siteIds = new ArrayList<>();
            List<GetSiteListDTO> sites = siteClient.getSiteListByUserId(userId).getData();
            if (sites != null) {
                for (GetSiteListDTO site : sites) {
                    siteIds.add(site.getId());
                }
            }
        if (siteIds.isEmpty()){
            siteIds.add(-1);
            chargingRefundDto.setSiteIds(siteIds.isEmpty() ? Collections.singletonList(-1) : siteIds);
        }
        // 查询当前登陆人按钮权限
        SysUser sysUser = sysUserClient.getSysUser(tokenService.getLoginUser().getUserid()).getData();
        Integer roleType = sysUser.getRoleType();
        List<TChargingOrderRefund> tChargingOrderRefunds = new ArrayList<>();
        for (TChargingOrderRefund record : refundList1.getData().getRecords()) {
            if (roleType==1){
                tChargingOrderRefunds.add(record);
                continue;
            }
            if (record.getType()!=1){
                tChargingOrderRefunds.add(record);
            }
            TChargingOrder byId = chargingOrderService.getById(record.getOrderId());
            record.setUid(record.getId().toString());
            if (byId==null){
                continue;
            }
            if (roleType==2&&siteIds.contains(byId.getSiteId())){
                tChargingOrderRefunds.add(record);
            }
        }
        PageInfo<TChargingOrderRefund> data = refundList1.getData();
        List<TChargingOrderRefund> res1 = testing4(tChargingOrderRefunds.size(), pageCurr, pageSize, tChargingOrderRefunds);
        data.setTotal(tChargingOrderRefunds.size());
        data.setRecords(res1);
        return refundList1;
        return chargingOrderService.getRefundList(chargingRefundDto);
    }
    
    
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java
@@ -21,10 +21,14 @@
import com.ruoyi.order.api.model.TShoppingOrder;
import com.ruoyi.order.api.model.TVipOrder;
import com.ruoyi.order.api.model.TVipOrderRefund;
import com.ruoyi.order.api.query.ChargingOrderQuery;
import com.ruoyi.order.api.query.ShoppingOrderQuery;
import com.ruoyi.order.api.query.VipShoppingOrderQuery;
import com.ruoyi.order.api.vo.ChargingOrderVO;
import com.ruoyi.order.dto.TEnterpriseExport;
import com.ruoyi.order.export.EnterpriseChargingDetailExport;
import com.ruoyi.order.export.TVipOrderExport;
import com.ruoyi.order.service.TChargingOrderService;
import com.ruoyi.order.service.TVipOrderRefundService;
import com.ruoyi.order.service.TVipOrderService;
import com.ruoyi.other.api.domain.TEnterpriseUserApplication;
@@ -69,6 +73,8 @@
    private OtherClient otherClient;
    @Resource
    private TVipOrderRefundService vipOrderRefundService;
    @Resource
    private TChargingOrderService chargingOrderService;
    
    
    
@@ -236,40 +242,73 @@
    }
    @RequiresPermissions(value = {"/vipList", "/vipList/export"}, logical = Logical.OR)
    @ApiOperation(tags = {"管理后台-会员列表"}, value = "导出会员充值订单")
    @ApiOperation(tags = {"管理后台-会员列表"}, value = "导出会员充电订单")
    @PutMapping(value = "/exportVipOrderByVipUser")
    @Log(title = "会员列表导出会员充值订单", businessType = BusinessType.EXPORT)
    @Log(title = "会员列表导出会员充电订单", businessType = BusinessType.EXPORT)
    public void exportVipOrderByVipUser(@RequestBody UserListQueryDto query) {
        List<Long> userIds = appUserClient.getVipUserIds(query).getData();
        if (userIds == null || userIds.isEmpty()) {
            writeVipOrderWorkbook(new ArrayList<>());
            writeVipChargingOrderWorkbook(new ArrayList<>());
            return;
        }
        VipShoppingOrderQuery orderQuery = new VipShoppingOrderQuery();
        ChargingOrderQuery orderQuery = new ChargingOrderQuery();
        orderQuery.setUserIds(userIds);
        if (query != null) {
            orderQuery.setVipId(query.getVipTypeId());
        }
        orderQuery.setPageCurr(1);
        orderQuery.setPageSize(100000);
        List<TVipOrder> list = vipOrderService.pageList(orderQuery).getRecords();
        List<TVipOrderExport> exports = new ArrayList<>();
        for (TVipOrder vipOrder : list) {
            TVipOrderExport export = new TVipOrderExport();
            BeanUtils.copyProperties(vipOrder, export);
            export.setCreateTime(DateUtils.localDateTimeToString(vipOrder.getCreateTime()));
        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(DateUtils.localDateTimeToString(order.getStartTime()));
            export.setEndTime(DateUtils.localDateTimeToString(order.getEndTime()));
            exports.add(export);
        }
        writeVipOrderWorkbook(exports);
        writeVipChargingOrderWorkbook(exports);
    }
    private String getChargingOrderStatus(Integer status) {
        if (status == null) return "未知";
        String[] names = {"未知", "等待中", "启动中", "充电中", "停止中", "已结束"};
        return status >= 0 && status < names.length ? names[status] : "未知";
    }
    private 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 void writeVipChargingOrderWorkbook(List<EnterpriseChargingDetailExport> exports) {
        Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), EnterpriseChargingDetailExport.class, exports);
        writeWorkbook(workbook, "会员充电订单导出.xls");
    }
    private void writeVipOrderWorkbook(List<TVipOrderExport> exports) {
        Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), TVipOrderExport.class, exports);
        writeWorkbook(workbook, "会员订单导出.xls");
    }
    private void writeWorkbook(Workbook workbook, String exportFileName) {
        HttpServletResponse response = WebUtils.response();
        response.setCharacterEncoding("utf-8");
        ServletOutputStream outputStream = null;
        try {
            String fileName = URLEncoder.encode("会员订单导出.xls", "utf-8");
            String fileName = URLEncoder.encode(exportFileName, "utf-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
            response.setContentType("application/vnd.ms-excel;charset=UTF-8");
            response.setHeader("Pragma", "no-cache");
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/ChargingRefundDto.java
@@ -4,6 +4,8 @@
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
public class ChargingRefundDto extends BasePage {
    @ApiModelProperty("订单编号")
@@ -14,4 +16,6 @@
    private String refundCode;
    @ApiModelProperty("是否特殊退款处理")
    private Boolean specialRefund;
    @ApiModelProperty(value = "可查看站点ID", hidden = true)
    private List<Integer> siteIds;
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/EnterpriseChargingDetailVO.java
@@ -59,6 +59,24 @@
    @ApiModelProperty("Phone")
    private String phone;
    @ApiModelProperty("License plate")
    private String licensePlate;
    @ApiModelProperty("Charging capacity")
    private BigDecimal chargingCapacity;
    @ApiModelProperty("Electricity fee")
    private BigDecimal electrovalence;
    @ApiModelProperty("Service fee")
    private BigDecimal serviceCharge;
    @ApiModelProperty("Charging end mode")
    private Integer endMode;
    @ApiModelProperty("Accounting period count")
    private Integer periodCount;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java
@@ -30,19 +30,19 @@
    @Excel(name = "充电时长(秒)", width = 20)
    private Long chargingSecond;
    @Excel(name = "总金额", width = 20)
    @Excel(name = "总金额", width = 20, numFormat = "0.00")
    private BigDecimal totalAmount;
    @Excel(name = "优惠券抵扣", width = 20)
    @Excel(name = "优惠券抵扣", width = 20, numFormat = "0.00")
    private BigDecimal couponDiscountAmount;
    @Excel(name = "会员抵扣", width = 20)
    @Excel(name = "会员抵扣", width = 20, numFormat = "0.00")
    private BigDecimal vipDiscountAmount;
    @Excel(name = "实付金额", width = 20)
    @Excel(name = "实付金额", width = 20, numFormat = "0.00")
    private BigDecimal paymentAmount;
    @Excel(name = "退款金额", width = 20)
    @Excel(name = "退款金额", width = 20, numFormat = "0.00")
    private BigDecimal refundAmount;
    @Excel(name = "归属合作商/服务商", width = 30)
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
@@ -75,8 +75,14 @@
    List<Map<String, Object>> enterpriseTopSites(@Param("data") EnterpriseChargingAnalysisQuery query);
    List<Map<String, Object>> enterpriseTopSitesByCompanies(@Param("data") EnterpriseChargingAnalysisQuery query,
                                                             @Param("companyNames") List<String> companyNames);
    List<LocalDateTime> enterpriseChargingTimes(@Param("data") EnterpriseChargingAnalysisQuery query);
    List<Map<String, Object>> enterpriseChargingTimesByCompanies(@Param("data") EnterpriseChargingAnalysisQuery query,
                                                                  @Param("companyNames") List<String> companyNames);
    List<EnterpriseChargingDetailVO> enterpriseChargingDetail(@Param("pageInfo") PageInfo<EnterpriseChargingDetailVO> pageInfo,
                                                              @Param("data") EnterpriseChargingAnalysisQuery query);
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -2979,22 +2979,68 @@
        pageInfo.setRecords(list);
        return R.ok(pageInfo);
    }
        @Override
    @Override
    public R<PageInfo<EnterpriseChargingAnalysisVO>> enterpriseChargingAnalysis(EnterpriseChargingAnalysisQuery query) {
        PageInfo<EnterpriseChargingAnalysisVO> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
        List<EnterpriseChargingAnalysisVO> list = this.baseMapper.enterpriseChargingAnalysis(pageInfo, query);
        if (list == null || list.isEmpty()) {
            pageInfo.setRecords(Collections.emptyList());
            return R.ok(pageInfo);
        }
        List<String> companyNames = list.stream()
                .map(EnterpriseChargingAnalysisVO::getCompanyName)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        Map<String, List<Map<String, Object>>> topSitesByCompany = groupByCompany(
                this.baseMapper.enterpriseTopSitesByCompanies(query, companyNames));
        Map<String, List<Map<String, Object>>> chargingTimesByCompany = groupByCompany(
                this.baseMapper.enterpriseChargingTimesByCompanies(query, companyNames));
        for (EnterpriseChargingAnalysisVO row : list) {
            EnterpriseChargingAnalysisQuery rowQuery = buildEnterpriseRowQuery(query, row);
            List<Map<String, Object>> topSites = this.baseMapper.enterpriseTopSites(rowQuery);
            String companyKey = companyKey(row.getCompanyName(), row.getSocialCreditCode());
            List<Map<String, Object>> topSites = topSitesByCompany.getOrDefault(companyKey, Collections.emptyList());
            String topSiteNames = topSites.stream()
                    .sorted(Comparator.comparing((Map<String, Object> item) -> ((Number) item.getOrDefault("counts", 0)).longValue()).reversed())
                    .limit(3)
                    .map(item -> Objects.toString(item.get("siteName"), ""))
                    .filter(org.springframework.util.StringUtils::hasText)
                    .collect(Collectors.joining("、"));
            row.setTopSiteNames(topSiteNames);
            row.setAverageChargingInterval(calculateAverageChargingInterval(this.baseMapper.enterpriseChargingTimes(rowQuery)));
            List<LocalDateTime> chargingTimes = chargingTimesByCompany.getOrDefault(companyKey, Collections.emptyList()).stream()
                    .map(item -> toLocalDateTime(item.get("startTime")))
                    .filter(Objects::nonNull)
                    .sorted()
                    .collect(Collectors.toList());
            row.setAverageChargingInterval(calculateAverageChargingInterval(chargingTimes));
        }
        pageInfo.setRecords(list);
        return R.ok(pageInfo);
    }
    private Map<String, List<Map<String, Object>>> groupByCompany(List<Map<String, Object>> rows) {
        if (rows == null || rows.isEmpty()) {
            return Collections.emptyMap();
        }
        return rows.stream().collect(Collectors.groupingBy(row -> companyKey(
                Objects.toString(row.get("companyName"), null),
                Objects.toString(row.get("socialCreditCode"), ""))));
    }
    private String companyKey(String companyName, String socialCreditCode) {
        return Objects.toString(companyName, "") + "\u0000" + Objects.toString(socialCreditCode, "");
    }
    private LocalDateTime toLocalDateTime(Object value) {
        if (value instanceof LocalDateTime) {
            return (LocalDateTime) value;
        }
        if (value instanceof java.sql.Timestamp) {
            return ((java.sql.Timestamp) value).toLocalDateTime();
        }
        if (value instanceof Date) {
            return LocalDateTime.ofInstant(((Date) value).toInstant(), ZoneId.systemDefault());
        }
        return null;
    }
    @Override
@@ -3023,22 +3069,25 @@
        if (times == null || times.size() < 2) {
            return BigDecimal.ZERO;
        }
        long totalMinutes = 0L;
        long totalSeconds = 0L;
        for (int i = 1; i < times.size(); i++) {
            long minutes = ChronoUnit.MINUTES.between(times.get(i - 1), times.get(i));
            if (minutes > 0) {
                totalMinutes += minutes;
            long seconds = Duration.between(times.get(i - 1), times.get(i)).getSeconds();
            if (seconds > 0) {
                totalSeconds += seconds;
            }
        }
        return BigDecimal.valueOf(totalMinutes)
        return BigDecimal.valueOf(totalSeconds)
                .divide(BigDecimal.valueOf(times.size() - 1L), 4, RoundingMode.HALF_UP)
                .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
                .divide(BigDecimal.valueOf(3600), 2, RoundingMode.HALF_UP);
    }
    @Override
    public R<PageInfo<TChargingOrderRefund>> getRefundList(ChargingRefundDto chargingRefundDto) {
        PageInfo<TChargingOrderRefund> pageInfo = new PageInfo<>(chargingRefundDto.getPageCurr(), chargingRefundDto.getPageSize());
        List<TChargingOrderRefund> list = this.baseMapper.getRefundList(pageInfo, chargingRefundDto);
        for (TChargingOrderRefund refund : list) {
            refund.setUid(refund.getType() + "-" + refund.getId());
        }
        pageInfo.setRecords(list);
        
        return R.ok(pageInfo);
@@ -4597,33 +4646,7 @@
                        this.updateById(chargingOrder);
                        triggerChargingOrderShare(chargingOrder);
                    } else {
                        //构建退款明细
                        TChargingOrderRefund chargingOrderRefund = new TChargingOrderRefund();
                        chargingOrderRefund.setChargingOrderId(chargingOrder.getId());
                        SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyMMddHHmmssSSS");
                        chargingOrderRefund.setRefundCode("CDF" + sdf1.format(new Date()) + (Double.valueOf(Math.random() * 1000).intValue()));
                        chargingOrderRefund.setRefundAmount(refundAmount);
                        chargingOrderRefund.setBalanceRefundAmount(balanceRefundAmount);
                        chargingOrderRefund.setThirdRefundAmount(thirdRefundAmount);
                        chargingOrderRefund.setRefundStatus(2);
                        chargingOrderRefund.setPayType(rechargePaymentType);
                        chargingOrderRefund.setCode(chargingOrder.getCode());
                        chargingOrderRefund.setRefundTitle("充电完成退款");
                        chargingOrderRefund.setRefundContent("充电完成退款");
                        chargingOrderRefund.setRefundReason("充电完成退款");
                        chargingOrderRefund.setRefundRemark("实际充电消费金额:" + chargingOrder.getPaymentAmount());
                        chargingOrderRefund.setRefundTotalAmount(refundAmount);
                        chargingOrderRefund.setPayAmount(rechargeAmount);
                        chargingOrderRefund.setRefundTime(LocalDateTime.now());
                        chargingOrderRefundService.save(chargingOrderRefund);
                        if (balanceRefundAmount.compareTo(BigDecimal.ZERO) > 0) {
                            changeUserBalance(chargingOrder.getAppUserId(), balanceRefundAmount);
                            recordBalanceRefund(chargingOrder, balanceRefundAmount, chargingOrderRefund.getRefundCode());
                        }
                        chargingOrder.setRefundStatus(2);
                        chargingOrder.setRefundTime(LocalDateTime.now());
                        this.updateById(chargingOrder);
                        // 无可退金额时正常结算,不生成金额为 0 的退款记录。
                        triggerChargingOrderShare(chargingOrder);
                    }
                }
@@ -4775,6 +4798,7 @@
        if (order == null) {
            return R.fail("未查询到支付成功的充电订单");
        }
        mergeSuccessfulRefundAmounts(order);
        PayOrderInfoDto dto = new PayOrderInfoDto();
        dto.setOrderId(order.getId().toString());
        dto.setCode(order.getCode());
@@ -4816,6 +4840,7 @@
            if (order == null) {
                return R.fail("充电订单不存在");
            }
            mergeSuccessfulRefundAmounts(order);
            BigDecimal refundAmount = money(payOrderQueryDto.getRefundAmount());
            BigDecimal refundableAmount = money(order.getPaymentAmount()).subtract(money(order.getRefundAmount())).setScale(2, RoundingMode.HALF_UP);
            if (refundAmount.compareTo(refundableAmount) > 0) {
@@ -4882,6 +4907,40 @@
        }
    }
    private void mergeSuccessfulRefundAmounts(TChargingOrder order) {
        if (order == null || order.getId() == null) {
            return;
        }
        List<TChargingOrderRefund> refunds = chargingOrderRefundService.list(
                new LambdaQueryWrapper<TChargingOrderRefund>()
                        .eq(TChargingOrderRefund::getChargingOrderId, order.getId())
                        .eq(TChargingOrderRefund::getRefundStatus, 2));
        if (refunds == null || refunds.isEmpty()) {
            return;
        }
        BigDecimal successfulAmount = BigDecimal.ZERO;
        BigDecimal explicitBalanceAmount = BigDecimal.ZERO;
        BigDecimal explicitThirdAmount = BigDecimal.ZERO;
        BigDecimal unspecifiedAmount = BigDecimal.ZERO;
        for (TChargingOrderRefund refund : refunds) {
            BigDecimal amount = money(refund.getRefundAmount());
            BigDecimal balanceAmount = money(refund.getBalanceRefundAmount());
            BigDecimal thirdAmount = money(refund.getThirdRefundAmount());
            successfulAmount = successfulAmount.add(amount);
            explicitBalanceAmount = explicitBalanceAmount.add(balanceAmount);
            explicitThirdAmount = explicitThirdAmount.add(thirdAmount);
            if (balanceAmount.add(thirdAmount).compareTo(BigDecimal.ZERO) == 0) {
                unspecifiedAmount = unspecifiedAmount.add(amount);
            }
        }
        BigDecimal inferredThirdAmount = unspecifiedAmount.min(
                getRefundableThirdPayAmount(order).subtract(explicitThirdAmount).max(BigDecimal.ZERO));
        BigDecimal inferredBalanceAmount = unspecifiedAmount.subtract(inferredThirdAmount).max(BigDecimal.ZERO);
        order.setRefundAmount(money(order.getRefundAmount()).max(money(successfulAmount)));
        order.setThirdRefundAmount(money(order.getThirdRefundAmount()).max(money(explicitThirdAmount.add(inferredThirdAmount))));
        order.setBalanceRefundAmount(money(order.getBalanceRefundAmount()).max(money(explicitBalanceAmount.add(inferredBalanceAmount))));
    }
    private R refundIcbcShareBeforeMainRefund(TChargingOrder order, TChargingOrderRefund refund, BigDecimal thirdRefundAmount) {
        if (order == null || refund == null || thirdRefundAmount == null || thirdRefundAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return R.ok();
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TOrderInvoiceServiceImpl.java
@@ -11,6 +11,7 @@
import com.ruoyi.account.api.model.TAppUserCar;
import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
import com.ruoyi.chargingPile.api.model.Partner;
import com.ruoyi.chargingPile.api.model.Site;
import com.ruoyi.common.core.utils.StringUtils;
@@ -30,6 +31,8 @@
import com.ruoyi.other.api.feignClient.CouponClient;
import com.ruoyi.other.api.feignClient.GoodsClient;
import com.ruoyi.other.api.feignClient.InvoiceTypeClient;
import com.ruoyi.system.api.feignClient.SysUserClient;
import com.ruoyi.system.api.domain.SysUser;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
@@ -87,6 +90,9 @@
    private CouponClient couponClient;
    @Resource
    private AppUserClient appUserClient;
    @Resource
    private SysUserClient sysUserClient;
    
    @Resource
    private TChargingOrderAccountingStrategyService chargingOrderAccountingStrategyService;
@@ -375,6 +381,7 @@
    @Override
    public PageInfo<TOrderInvoiceVO> pageList(TOrderInvoiceQuery query) {
        applyPartnerSiteScope(query);
        List<TAppUser> tAppUsers = new ArrayList<>();
        // 查询申请用户
        if (org.springframework.util.StringUtils.hasLength(query.getUserPhone())){
@@ -446,6 +453,7 @@
    @Override
    public Map<String, Integer> statusCount(TOrderInvoiceQuery query) {
        applyPartnerSiteScope(query);
        Map<String, Integer> map = new HashMap<>();
        List<TAppUser> tAppUsers = new ArrayList<>();
@@ -466,6 +474,7 @@
    @Override
    public List<TOrderInvoiceVO> export(TOrderInvoiceQuery query) {
        applyPartnerSiteScope(query);
        List<TAppUser> tAppUsers = new ArrayList<>();
        // 查询申请用户
        if (org.springframework.util.StringUtils.hasLength(query.getUserPhone())){
@@ -495,6 +504,31 @@
        return list;
    }
    private void applyPartnerSiteScope(TOrderInvoiceQuery query) {
        if (query == null) {
            return;
        }
        Long userId = tokenService.getLoginUser().getUserid();
        SysUser sysUser = sysUserClient.getSysUser(userId).getData();
        if (sysUser == null || !Integer.valueOf(2).equals(sysUser.getRoleType())) {
            return;
        }
        List<GetSiteListDTO> sites = siteClient.getSiteListByUserId(userId).getData();
        List<Integer> siteIds = sites == null ? new ArrayList<>() : sites.stream()
                .map(GetSiteListDTO::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (query.getSiteId() != null) {
            siteIds = siteIds.contains(query.getSiteId())
                    ? Collections.singletonList(query.getSiteId())
                    : Collections.singletonList(-1);
        }
        if (siteIds.isEmpty()) {
            siteIds = Collections.singletonList(-1);
        }
        query.setSiteIds(siteIds);
    }
    private void fillInvoiceDetailAmount(TOrderInvoiceVO invoice, List<TOrderInvoiceDetail> details) {
        List<TOrderInvoiceDetail> currentDetails = details.stream()
                .filter(t -> t.getOrderInvoiceId().equals(invoice.getId()))
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
@@ -300,6 +300,7 @@
        co.create_time as pay_time,
        co.id as order_id,
        co.app_user_id as user_id,
        co.site_id as site_id,
        co.pay_method as pay_method,
        1 as type
        FROM t_charging_order_refund re
@@ -324,6 +325,7 @@
        so.create_time as pay_time,
        so.id as order_id,
        so.app_user_id as user_id,
        null as site_id,
        3 as pay_method,
        2 as type
        FROM t_shopping_order_refund re
@@ -348,6 +350,7 @@
        vo.create_time as pay_time,
        vo.id as order_id,
        vo.app_user_id as user_id,
        null as site_id,
        3 as pay_method,
        3 as type
        FROM t_vip_order_refund vr
@@ -365,6 +368,13 @@
            </if>
            <if test="data.specialRefund != null and data.specialRefund">
                AND o.refund_title = '特殊退款处理'
            </if>
            <if test="data.siteIds != null and data.siteIds.size() > 0">
                AND (o.type != 1 OR o.site_id IN
                <foreach collection="data.siteIds" item="siteId" open="(" separator="," close=")">
                    #{siteId}
                </foreach>
                )
            </if>
        </where>
        order by o.refund_time desc
@@ -2334,24 +2344,44 @@
    <select id="chargingOrderShareIncomeByOrderIds" resultType="map">
        select
        charging_order_id as orderId,
        sum(IFNULL(channel_pay_amount, 0) - IFNULL(share_amount, 0)) as platformIncome,
        sum(IFNULL(share_amount, 0)) as partnerIncome
        from `charging_pile_order`.t_charging_order_share_record
        where del_flag = 0 and charging_order_id is not null
        s.charging_order_id as orderId,
        sum(
            IFNULL(s.channel_pay_amount, 0)
            - IFNULL(o.third_refund_amount, CASE WHEN o.pay_method = 3 THEN IFNULL(o.refund_amount, 0) ELSE 0 END)
            - IFNULL(s.share_amount, 0)
                * (IFNULL(s.channel_pay_amount, 0)
                    - IFNULL(o.third_refund_amount, CASE WHEN o.pay_method = 3 THEN IFNULL(o.refund_amount, 0) ELSE 0 END))
                / IFNULL(s.channel_pay_amount, 0)
        ) as platformIncome,
        sum(
            IFNULL(s.share_amount, 0)
            * (IFNULL(s.channel_pay_amount, 0)
                - IFNULL(o.third_refund_amount, CASE WHEN o.pay_method = 3 THEN IFNULL(o.refund_amount, 0) ELSE 0 END))
            / IFNULL(s.channel_pay_amount, 0)
        ) as partnerIncome
        from `charging_pile_order`.t_charging_order_share_record s
        inner join `charging_pile_order`.t_charging_order o on o.id = s.charging_order_id
        where s.del_flag = 0 and s.charging_order_id is not null
        and IFNULL(o.third_refund_amount, CASE WHEN o.pay_method = 3 THEN IFNULL(o.refund_amount, 0) ELSE 0 END)
            &lt; IFNULL(s.channel_pay_amount, 0)
        <if test="orderIds != null and orderIds.size()>0">
            and charging_order_id in
            and s.charging_order_id in
            <foreach collection="orderIds" item="item" index="index" separator="," open="(" close=")">
                #{item}
            </foreach>
        </if>
        group by charging_order_id
        group by s.charging_order_id
    </select>
    <select id="chargingOrderShareIncomeByPayChannel" resultType="map">
        select
        s.pay_channel as payChannel,
        sum(IFNULL(s.share_amount, 0)) as shareAmount
        sum(
            IFNULL(s.share_amount, 0)
            * (IFNULL(s.channel_pay_amount, 0)
                - IFNULL(o.third_refund_amount, CASE WHEN o.pay_method = 3 THEN IFNULL(o.refund_amount, 0) ELSE 0 END))
            / IFNULL(s.channel_pay_amount, 0)
        ) as shareAmount
        from t_charging_order_share_record s
        inner join t_charging_order o on s.charging_order_id = o.id
        where s.del_flag = 0
@@ -2359,6 +2389,8 @@
        and o.del_flag = 0
        and o.recharge_payment_status = 2
        and o.status = 5
        and IFNULL(o.third_refund_amount, CASE WHEN o.pay_method = 3 THEN IFNULL(o.refund_amount, 0) ELSE 0 END)
            &lt; IFNULL(s.channel_pay_amount, 0)
        <if test="sixBefore != null">
            and o.create_time &gt;= #{sixBefore}
        </if>
@@ -2373,12 +2405,27 @@
    <select id="chargingOrderShareIncomeSummaryByOrderIds" resultType="map">
        select
        sum(IFNULL(channel_pay_amount, 0) - IFNULL(share_amount, 0)) as platformIncomeTotal,
        sum(IFNULL(share_amount, 0)) as partnerIncomeTotal
        from `charging_pile_order`.t_charging_order_share_record
        where del_flag = 0 and charging_order_id is not null
        sum(
            IFNULL(s.channel_pay_amount, 0)
            - IFNULL(o.third_refund_amount, CASE WHEN o.pay_method = 3 THEN IFNULL(o.refund_amount, 0) ELSE 0 END)
            - IFNULL(s.share_amount, 0)
                * (IFNULL(s.channel_pay_amount, 0)
                    - IFNULL(o.third_refund_amount, CASE WHEN o.pay_method = 3 THEN IFNULL(o.refund_amount, 0) ELSE 0 END))
                / IFNULL(s.channel_pay_amount, 0)
        ) as platformIncomeTotal,
        sum(
            IFNULL(s.share_amount, 0)
            * (IFNULL(s.channel_pay_amount, 0)
                - IFNULL(o.third_refund_amount, CASE WHEN o.pay_method = 3 THEN IFNULL(o.refund_amount, 0) ELSE 0 END))
            / IFNULL(s.channel_pay_amount, 0)
        ) as partnerIncomeTotal
        from `charging_pile_order`.t_charging_order_share_record s
        inner join `charging_pile_order`.t_charging_order o on o.id = s.charging_order_id
        where s.del_flag = 0 and s.charging_order_id is not null
        and IFNULL(o.third_refund_amount, CASE WHEN o.pay_method = 3 THEN IFNULL(o.refund_amount, 0) ELSE 0 END)
            &lt; IFNULL(s.channel_pay_amount, 0)
        <if test="orderIds != null and orderIds.size()>0">
            and charging_order_id in
            and s.charging_order_id in
            <foreach collection="orderIds" item="item" index="index" separator="," open="(" close=")">
                #{item}
            </foreach>
@@ -2614,7 +2661,7 @@
    <sql id="enterpriseChargingBaseWhere">
        toi.invoicing_object_type = 2
        AND toi.status IN (2, 3)
        AND toi.status IN (1, 2, 3)
        AND tod.order_type = 1
        AND co.del_flag = 0
        AND co.recharge_payment_status = 2
@@ -2664,7 +2711,7 @@
                MAX(IFNULL(toi.total_amount, 0)) AS totalAmount,
                SUM(IFNULL(tod.invoice_amount, 0)) AS detailInvoiceAmount,
                COUNT(DISTINCT co.id) AS orderCount,
                MAX(co.end_time) AS latestChargingTime
                MAX(co.start_time) AS latestChargingTime
            FROM t_order_invoice toi
            INNER JOIN t_order_invoice_detail tod ON tod.order_invoice_id = toi.id
            INNER JOIN t_charging_order co ON co.id = tod.order_id
@@ -2695,6 +2742,27 @@
        LIMIT 3
    </select>
    <select id="enterpriseTopSitesByCompanies" resultType="map">
        SELECT
            toi.name AS companyName,
            IFNULL(toi.tax_identification_number, '') AS socialCreditCode,
            s.name AS siteName,
            COUNT(DISTINCT co.id) AS counts
        FROM t_order_invoice toi
        INNER JOIN t_order_invoice_detail tod ON tod.order_invoice_id = toi.id
        INNER JOIN t_charging_order co ON co.id = tod.order_id
        LEFT JOIN charging_pile_service.t_site s ON s.id = co.site_id
        <where>
            <include refid="enterpriseChargingBaseWhere" />
            AND s.name IS NOT NULL
            AND toi.name IN
            <foreach collection="companyNames" item="companyName" open="(" separator="," close=")">
                #{companyName}
            </foreach>
        </where>
        GROUP BY toi.name, toi.tax_identification_number, s.id, s.name
    </select>
    <select id="enterpriseChargingTimes" resultType="java.time.LocalDateTime">
        SELECT DISTINCT co.start_time
        FROM t_order_invoice toi
@@ -2706,6 +2774,26 @@
            AND co.start_time IS NOT NULL
        </where>
        ORDER BY co.start_time ASC
    </select>
    <select id="enterpriseChargingTimesByCompanies" resultType="map">
        SELECT DISTINCT
            toi.name AS companyName,
            IFNULL(toi.tax_identification_number, '') AS socialCreditCode,
            co.start_time AS startTime
        FROM t_order_invoice toi
        INNER JOIN t_order_invoice_detail tod ON tod.order_invoice_id = toi.id
        INNER JOIN t_charging_order co ON co.id = tod.order_id
        LEFT JOIN charging_pile_service.t_site s ON s.id = co.site_id
        <where>
            <include refid="enterpriseChargingBaseWhere" />
            AND co.start_time IS NOT NULL
            AND toi.name IN
            <foreach collection="companyNames" item="companyName" open="(" separator="," close=")">
                #{companyName}
            </foreach>
        </where>
        ORDER BY toi.name, co.start_time
    </select>
    <sql id="enterpriseChargingDetailWhere">
@@ -2760,6 +2848,12 @@
            co.order_source AS orderSource,
            p.name AS partnerName,
            au.phone AS phone,
            car.license_plate AS licensePlate,
            co.charging_capacity AS chargingCapacity,
            IFNULL(price.electrovalence, 0) AS electrovalence,
            IFNULL(price.serviceCharge, 0) AS serviceCharge,
            co.end_mode AS endMode,
            IFNULL(price.periodCount, 0) AS periodCount,
            co.create_time AS createTime,
            co.start_time AS startTime,
            co.end_time AS endTime,
@@ -2772,6 +2866,15 @@
        LEFT JOIN charging_pile_service.t_charging_pile cp ON cp.id = co.charging_pile_id
        LEFT JOIN charging_pile_service.t_charging_gun cg ON cg.id = co.charging_gun_id
        LEFT JOIN charging_pile_account.t_app_user au ON au.id = co.app_user_id
        LEFT JOIN charging_pile_account.t_app_user_car car ON car.id = co.app_user_car_id
        LEFT JOIN (
            SELECT charging_order_id,
                   SUM(period_electric_price) AS electrovalence,
                   SUM(period_service_price) AS serviceCharge,
                   COUNT(1) AS periodCount
            FROM t_charging_order_accounting_strategy
            GROUP BY charging_order_id
        ) price ON price.charging_order_id = co.id
    </sql>
    <select id="enterpriseChargingDetail" resultType="com.ruoyi.order.dto.EnterpriseChargingDetailVO">
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TOrderInvoiceMapper.xml
@@ -80,6 +80,12 @@
            <if test="query.siteId != null">
                AND toi.site_id = #{query.siteId}
            </if>
            <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.partnerId != null">
                AND toi.partner_id = #{query.partnerId}
            </if>
@@ -131,6 +137,12 @@
            </if>
            <if test="query.siteId != null">
                AND toi.site_id = #{query.siteId}
            </if>
            <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.partnerId != null">
                AND toi.partner_id = #{query.partnerId}
@@ -185,6 +197,12 @@
            <if test="query.siteId != null">
                AND toi.site_id = #{query.siteId}
            </if>
            <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.partnerId != null">
                AND toi.partner_id = #{query.partnerId}
            </if>
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/EnterpriseChargingDetailMapperTest.java
New file
@@ -0,0 +1,38 @@
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 EnterpriseChargingDetailMapperTest {
    @Test
    void selectsExpectedChargingDetailColumns() 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 detailSelect = mapper.substring(
                mapper.indexOf("<sql id=\"enterpriseChargingDetailSelect\""),
                mapper.indexOf("<select id=\"enterpriseChargingDetail\""));
        assertTrue(detailSelect.contains("AS licensePlate"));
        assertTrue(detailSelect.contains("AS chargingCapacity"));
        assertTrue(detailSelect.contains("AS electrovalence"));
        assertTrue(detailSelect.contains("AS serviceCharge"));
        assertTrue(detailSelect.contains("AS endMode"));
        assertTrue(detailSelect.contains("AS periodCount"));
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/PayOrderQueryPerformanceMapperTest.java
@@ -12,6 +12,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
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 PayOrderQueryPerformanceMapperTest {
@@ -35,6 +36,25 @@
        assertFalse(payOrderQuery.contains("t_vip_order_refund"));
    }
    @Test
    void refundListAppliesPartnerSiteScopeInsidePagedQuery() 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 refundQuery = mapper.substring(
                mapper.indexOf("<select id=\"getRefundList\""),
                mapper.indexOf("<select id=\"chargingOrderGroup\""));
        assertTrue(refundQuery.contains("data.siteIds"));
        assertTrue(refundQuery.contains("o.site_id IN"));
        assertEquals(3, countOccurrences(refundQuery, "as site_id"));
    }
    private int countOccurrences(String text, String target) {
        int count = 0;
        int index = 0;
sql/20260721_builtin_partner_role_permissions.sql
New file
@@ -0,0 +1,19 @@
-- 系统管理-角色:补齐合作商类型内置角色的菜单权限
-- 仅追加基础合作商角色已有的菜单,不覆盖管理员在目标角色上的自定义权限。
INSERT IGNORE INTO sys_role_menu (role_id, menu_id)
SELECT target_role.role_id, source_menu.menu_id
FROM sys_role target_role
JOIN sys_role_menu source_menu
  ON source_menu.role_id = (
    SELECT role_id
    FROM sys_role
    WHERE del_flag = '0'
      AND status = '0'
      AND role_name IN ('独立合作商', '合作商')
    ORDER BY CASE role_name WHEN '独立合作商' THEN 0 ELSE 1 END, role_id
    LIMIT 1
  )
WHERE target_role.del_flag = '0'
  AND target_role.status = '0'
  AND target_role.role_name IN ('独立合作商', '合作商', '场地方', '服务商');