新增企业用户充电分析功能页(包含充电明细);新增系统用户管理中的重置操作密码功能;在角色管理中增加独立合作商角色;角色管理中的添加角色操作新增数据驾驶舱和视频监控入口(需在角色管理员有按钮权限控制)。
4个文件已添加
9个文件已修改
843 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysRole.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysRoleController.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RoleInfoVo.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/EnterpriseChargingAnalysisQuery.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/EnterpriseChargingAnalysisVO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/EnterpriseChargingDetailVO.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysRole.java
@@ -73,6 +73,12 @@
    
    @TableField(exist = false)
    private Long number;
    @TableField(exist = false)
    private Boolean systemRole;
    @TableField(exist = false)
    private Boolean partnerRole;
    
    
    public SysRole()
@@ -214,6 +220,22 @@
    public void setNumber(Long number) {
        this.number = number;
    }
    public Boolean getSystemRole() {
        return systemRole;
    }
    public void setSystemRole(Boolean systemRole) {
        this.systemRole = systemRole;
    }
    public Boolean getPartnerRole() {
        return partnerRole;
    }
    public void setPartnerRole(Boolean partnerRole) {
        this.partnerRole = partnerRole;
    }
    
    @Override
    public String toString() {
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysRoleController.java
@@ -3,7 +3,9 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import javax.management.relation.Role;
@@ -61,6 +63,11 @@
@RequestMapping("/role")
@Api(tags = "角色模块")
public class SysRoleController extends BaseController {
    private static final String INDEPENDENT_PARTNER_ROLE_NAME = "独立合作商";
    private static final String INDEPENDENT_PARTNER_ROLE_KEY = "independent_partner";
    private static final String OWN_SITE_NAME = "自有电站";
    private static final String SYSTEM_ROLE_REMARK = "系统内置角色";
    @Resource
    private ISysRoleService roleService;
    
@@ -84,6 +91,88 @@
    
    @Resource
    private SiteClient siteClient;
    private void ensureIndependentPartnerRole() {
        SysRole role = roleService.getOne(new LambdaQueryWrapper<SysRole>()
                .eq(SysRole::getRoleKey, INDEPENDENT_PARTNER_ROLE_KEY)
                .eq(SysRole::getDelFlag, 0)
                .last("LIMIT 1"));
        if (role == null) {
            role = roleService.getOne(new LambdaQueryWrapper<SysRole>()
                    .eq(SysRole::getRoleName, INDEPENDENT_PARTNER_ROLE_NAME)
                    .eq(SysRole::getDelFlag, 0)
                    .last("LIMIT 1"));
        }
        if (role != null) {
            return;
        }
        SysRole newRole = new SysRole();
        newRole.setRoleName(INDEPENDENT_PARTNER_ROLE_NAME);
        newRole.setRoleKey(INDEPENDENT_PARTNER_ROLE_KEY);
        newRole.setDataScope("5");
        newRole.setStatus("0");
        newRole.setDelFlag("0");
        newRole.setRemark(SYSTEM_ROLE_REMARK);
        newRole.setCreateBy(SecurityUtils.getUsername());
        newRole.setCreateTime(new Date());
        Set<Long> menuIds = getIndependentPartnerMenuIds();
        newRole.setMenuIds(menuIds.toArray(new Long[0]));
        roleService.insertRole(newRole);
    }
    private Set<Long> getIndependentPartnerMenuIds() {
        List<SysMenus> menus = menuMapper.getAll();
        Set<Long> menuIds = new HashSet<>();
        Set<Long> rootIds = new HashSet<>();
        for (SysMenus menu : menus) {
            String path = menu.getPath() == null ? "" : menu.getPath();
            String menuName = menu.getMenuName() == null ? "" : menu.getMenuName();
            if ("standingPile".equals(path) || "order".equals(path)
                    || menuName.contains("站桩管理") || menuName.contains("订单管理")) {
                rootIds.add(menu.getMenuId());
            }
        }
        for (Long rootId : rootIds) {
            collectMenuTreeIds(rootId, menus, menuIds);
        }
        return menuIds;
    }
    private void collectMenuTreeIds(Long menuId, List<SysMenus> menus, Set<Long> menuIds) {
        if (menuId == null || menuIds.contains(menuId)) {
            return;
        }
        menuIds.add(menuId);
        for (SysMenus menu : menus) {
            if (menuId.equals(menu.getParentId())) {
                collectMenuTreeIds(menu.getMenuId(), menus, menuIds);
            }
        }
    }
    private boolean isIndependentPartnerRole(SysRole role) {
        return role != null && (INDEPENDENT_PARTNER_ROLE_KEY.equals(role.getRoleKey())
                || INDEPENDENT_PARTNER_ROLE_NAME.equals(role.getRoleName()));
    }
    private boolean isSystemBuiltinRole(SysRole role) {
        return role != null && (role.isAdmin() || isIndependentPartnerRole(role));
    }
    private void fillRoleV2Info(SysRole role) {
        if (role == null) {
            return;
        }
        boolean partnerRole = isIndependentPartnerRole(role);
        role.setPartnerRole(partnerRole);
        role.setSystemRole(isSystemBuiltinRole(role));
        if (partnerRole) {
            role.setSiteNames(Arrays.asList(OWN_SITE_NAME));
            if (StringUtils.isEmpty(role.getRemark())) {
                role.setRemark(SYSTEM_ROLE_REMARK);
            }
        }
    }
    
    
    
@@ -102,6 +191,7 @@
    @GetMapping("/listPage")
    @ApiOperation(value = "获取角色列表", tags = {"管理后台-角色管理"})
    public AjaxResult listPage(String name, BasePage basePage) {
        ensureIndependentPartnerRole();
        PageInfo<SysRole> pageInfo = new PageInfo<>(basePage.getPageCurr(), basePage.getPageSize());
        LambdaQueryWrapper<SysRole> wrapper = new LambdaQueryWrapper<SysRole>().eq(SysRole::getStatus, 0).eq(SysRole::getDelFlag, 0);
        if (StringUtils.isNotEmpty(name)) {
@@ -118,6 +208,7 @@
            }
            long count = sysUserRoleService.count(new LambdaQueryWrapper<SysUserRole>().eq(SysUserRole::getRoleId, record.getRoleId()));
            record.setNumber(count);
            fillRoleV2Info(record);
        }
        return AjaxResult.success(page);
    }
@@ -169,6 +260,9 @@
        RoleInfoVo roleInfoVo = new RoleInfoVo();
        roleInfoVo.setRoleId(role.getRoleId());
        roleInfoVo.setRoleName(role.getRoleName());
        roleInfoVo.setRoleKey(role.getRoleKey());
        roleInfoVo.setPartnerRole(isIndependentPartnerRole(role));
        roleInfoVo.setSystemRole(isSystemBuiltinRole(role));
        // 获取当前角色的菜单id
        List<Long> menusId = sysRoleMenuMapper.selectList(new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRoleId, id)).stream().map(SysRoleMenu::getMenuId).collect(Collectors.toList());
        if (menusId.size() == 0) {
@@ -194,12 +288,20 @@
        }
        roleInfoVo.setMenus(menusId);
        roleInfoVo.setRemark(role.getRemark());
        List<Integer> siteIds = roleSiteClient.getSiteIds(role.getRoleId()).getData();
        if (null != siteIds && siteIds.size() > 0) {
            List<Site> sites = siteClient.getSiteByIds(siteIds).getData();
            List<String> siteNames = sites.stream().map(Site::getName).collect(Collectors.toList());
            roleInfoVo.setSiteNames(siteNames);
            roleInfoVo.setSiteIds(siteIds);
        if (isIndependentPartnerRole(role)) {
            roleInfoVo.setSiteNames(Arrays.asList(OWN_SITE_NAME));
            roleInfoVo.setSiteIds(new ArrayList<>());
            if (StringUtils.isEmpty(roleInfoVo.getRemark())) {
                roleInfoVo.setRemark(SYSTEM_ROLE_REMARK);
            }
        } else {
            List<Integer> siteIds = roleSiteClient.getSiteIds(role.getRoleId()).getData();
            if (null != siteIds && siteIds.size() > 0) {
                List<Site> sites = siteClient.getSiteByIds(siteIds).getData();
                List<String> siteNames = sites.stream().map(Site::getName).collect(Collectors.toList());
                roleInfoVo.setSiteNames(siteNames);
                roleInfoVo.setSiteIds(siteIds);
            }
        }
        return AjaxResult.success(roleInfoVo);
    }
@@ -209,6 +311,10 @@
    @PostMapping("/roleUpdate")
    @ApiOperation(value = "编辑角色", tags = {"管理后台-角色管理"})
    public AjaxResult roleUpdate(@Validated @RequestBody RoleUpdateDto dto) {
        SysRole oldRole = roleService.selectRoleById(dto.getRoleId());
        if (isSystemBuiltinRole(oldRole)) {
            return AjaxResult.error("系统内置角色不可编辑");
        }
        SysRole role = new SysRole();
        role.setRoleName(dto.getRoleName());
        SysRole one = roleService.getOne(new LambdaQueryWrapper<SysRole>().eq(SysRole::getRoleName, dto.getRoleName()));
@@ -269,6 +375,12 @@
    @DeleteMapping("/{roleIds}")
    @ApiOperation(value = "删除角色", tags = {"管理后台-角色管理"})
    public AjaxResult remove(@PathVariable Long[] roleIds) {
        for (Long roleId : roleIds) {
            SysRole role = roleService.selectRoleById(roleId);
            if (isSystemBuiltinRole(role)) {
                return AjaxResult.error("系统内置角色不可删除");
            }
        }
        return toAjax(roleService.deleteRoleByIds(roleIds));
    }
    
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java
@@ -1,6 +1,7 @@
package com.ruoyi.system.controller;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.Resource;
@@ -8,8 +9,12 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
import com.ruoyi.chargingPile.api.model.Site;
import com.ruoyi.common.core.constant.MsgConstants;
import com.ruoyi.common.core.utils.HttpUtils;
import com.ruoyi.common.core.utils.MsgUtil;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.common.security.annotation.Logical;
import com.ruoyi.other.api.domain.TUserSite;
@@ -48,6 +53,9 @@
@RequestMapping("/user")
@Api(tags = "用户信息")
public class SysUserController extends BaseController {
    private static final String RESET_PWD_CODE_KEY = "system:user:resetPwd:code:";
    private static final String RESET_PWD_LIMIT_KEY = "system:user:resetPwd:limit:";
    @Resource
    private ISysUserService userService;
@@ -80,6 +88,9 @@
    
    @Resource
    private SiteClient siteClient;
    @Resource
    private RedisService redisService;
    
    
    
@@ -541,18 +552,92 @@
    /**
     * 发送重置密码验证码
     */
    @RequiresPermissions(value = {"/systemUserManagement/reset_passwords"}, logical = Logical.OR)
    @PostMapping("/resetPwd/sendCode")
    @ApiOperation(value = "发送重置密码验证码", tags = {"管理后台-系统用户管理"})
    public AjaxResult sendResetPwdCode(@RequestBody Map<String, Object> params) {
        Long userId = parseLong(params.get("userId"));
        if (userId == null) {
            return AjaxResult.error("用户不存在");
        }
        SysUser user = userService.selectUserById(userId);
        if (user == null) {
            return AjaxResult.error("用户不存在");
        }
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(userId);
        if (StringUtils.isEmpty(user.getPhonenumber())) {
            return AjaxResult.error("当前用户未维护手机号");
        }
        String limitKey = RESET_PWD_LIMIT_KEY + userId;
        if (Boolean.TRUE.equals(redisService.hasKey(limitKey))) {
            return AjaxResult.error("验证码发送过于频繁,请30秒后再试");
        }
        String code = MsgUtil.createCode();
        String reqStr = MsgUtil.codeMsg(user.getPhonenumber(), code);
        HttpUtils.post(MsgConstants.SEND_URL, reqStr);
        redisService.setCacheObject(RESET_PWD_CODE_KEY + userId, code, 5L, TimeUnit.MINUTES);
        redisService.setCacheObject(limitKey, "1", 30L, TimeUnit.SECONDS);
        return AjaxResult.success("验证码已发送");
    }
    /**
     * 重置密码
     */
    @RequiresPermissions(value = {"/systemUserManagement/reset_passwords"}, logical = Logical.OR)
    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
    @PutMapping("/resetPwd")
    @ApiOperation(value = "重置密码", tags = {"管理后台-系统用户管理"})
    public AjaxResult resetPwd(@RequestBody SysUser user) {
    public AjaxResult resetPwd(@RequestBody Map<String, Object> params) {
        Long userId = parseLong(params.get("userId"));
        String password = parseString(params.get("password"));
        String code = parseString(params.get("code"));
        if (userId == null) {
            return AjaxResult.error("用户不存在");
        }
        if (StringUtils.isEmpty(password)) {
            return AjaxResult.error("请输入新密码");
        }
        if (StringUtils.isEmpty(code)) {
            return AjaxResult.error("请输入验证码");
        }
        SysUser user = userService.selectUserById(userId);
        if (user == null) {
            return AjaxResult.error("用户不存在");
        }
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(user.getUserId());
        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
        userService.checkUserDataScope(userId);
        String cacheCode = redisService.getCacheObject(RESET_PWD_CODE_KEY + userId);
        if (StringUtils.isEmpty(cacheCode)) {
            return AjaxResult.error("验证码已过期,请重新获取验证码");
        }
        if (!code.equals(cacheCode)) {
            return AjaxResult.error("验证码错误");
        }
        user.setPassword(SecurityUtils.encryptPassword(password));
        user.setUpdateBy(SecurityUtils.getUsername());
        return toAjax(userService.resetPwd(user));
        int result = userService.resetPwd(user);
        redisService.deleteObject(RESET_PWD_CODE_KEY + userId);
        return toAjax(result);
    }
    private Long parseLong(Object value) {
        if (value == null || StringUtils.isEmpty(String.valueOf(value))) {
            return null;
        }
        return Long.valueOf(String.valueOf(value));
    }
    private String parseString(Object value) {
        return value == null ? null : String.valueOf(value);
    }
    /**
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RoleInfoVo.java
@@ -13,6 +13,12 @@
    private String roleName;
    private String roleKey;
    private Boolean systemRole;
    private Boolean partnerRole;
    @ApiModelProperty("菜单id")
    private List<Long> menus;
    
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -53,6 +53,7 @@
import com.ruoyi.order.dto.OrderEvaluateVo;
import com.ruoyi.order.dto.*;
import com.ruoyi.order.export.PayOrderExport;
import com.ruoyi.order.export.EnterpriseChargingDetailExport;
import com.ruoyi.order.service.*;
import com.ruoyi.order.util.PreviousSixMonths;
import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
@@ -217,6 +218,104 @@
    }
    
    @RequiresPermissions(value = {"/chargingUserAnalysis"}, logical = Logical.OR)
    @ResponseBody
    @PostMapping(value = "/enterprise/charging/analysis")
    @ApiOperation(value = "企业用户充电分析", tags = {"管理后台-数据分析-企业用户充电分析"})
    public R<PageInfo<EnterpriseChargingAnalysisVO>> enterpriseChargingAnalysis(@RequestBody EnterpriseChargingAnalysisQuery query) {
        return chargingOrderService.enterpriseChargingAnalysis(query);
    }
    @RequiresPermissions(value = {"/chargingUserAnalysis"}, logical = Logical.OR)
    @ResponseBody
    @PostMapping(value = "/enterprise/charging/detail")
    @ApiOperation(value = "企业用户充电明细", tags = {"管理后台-数据分析-企业用户充电分析"})
    public R<PageInfo<EnterpriseChargingDetailVO>> enterpriseChargingDetail(@RequestBody EnterpriseChargingAnalysisQuery query) {
        return chargingOrderService.enterpriseChargingDetail(query);
    }
    @RequiresPermissions(value = {"/chargingUserAnalysis"}, logical = Logical.OR)
    @ResponseBody
    @PutMapping(value = "/enterprise/charging/detail/export")
    @ApiOperation(value = "企业用户充电明细导出", tags = {"管理后台-数据分析-企业用户充电分析"})
    public void exportEnterpriseChargingDetail(@RequestBody EnterpriseChargingAnalysisQuery query) {
        List<EnterpriseChargingDetailVO> records = chargingOrderService.enterpriseChargingDetailExport(query);
        List<EnterpriseChargingDetailExport> exports = records.stream().map(this::buildEnterpriseChargingDetailExport).collect(Collectors.toList());
        Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), EnterpriseChargingDetailExport.class, exports);
        HttpServletResponse response = WebUtils.response();
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        try {
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("企业用户充电明细.xls", "utf-8"));
            try (ServletOutputStream outputStream = response.getOutputStream()) {
                workbook.write(outputStream);
            }
            workbook.close();
        } catch (IOException e) {
            log.error("导出企业用户充电明细失败", e);
        }
    }
    private EnterpriseChargingDetailExport buildEnterpriseChargingDetailExport(EnterpriseChargingDetailVO dto) {
        EnterpriseChargingDetailExport export = new EnterpriseChargingDetailExport();
        export.setCode(dto.getCode());
        export.setOrderType(formatEnterpriseOrderType(dto.getOrderType()));
        export.setOrderClassification(formatOrderClassification(dto.getOrderClassification()));
        export.setSiteName(dto.getSiteName());
        export.setTerminalName(dto.getTerminalName());
        export.setStatus(formatPayOrderStatus(1, dto.getStatus()));
        export.setChargingSecond(dto.getChargingSecond());
        export.setTotalAmount(dto.getTotalAmount());
        export.setCouponDiscountAmount(dto.getCouponDiscountAmount());
        export.setVipDiscountAmount(dto.getVipDiscountAmount());
        export.setPaymentAmount(dto.getPaymentAmount());
        export.setRefundAmount(dto.getRefundAmount());
        export.setPartnerName(dto.getPartnerName());
        export.setPhone(dto.getPhone());
        export.setOrderSource(formatOrderSource(dto.getOrderSource()));
        export.setStartTime(dto.getStartTime() == null ? null : DateUtils.localDateTimeToString(dto.getStartTime()));
        export.setEndTime(dto.getEndTime() == null ? null : DateUtils.localDateTimeToString(dto.getEndTime()));
        return export;
    }
    private String formatEnterpriseOrderType(Integer type) {
        if (type == null) {
            return "-";
        }
        if (type == 1) {
            return "充电订单(小程序)";
        }
        if (type == 2) {
            return "充电订单(刷卡)";
        }
        return "充电订单";
    }
    private String formatOrderClassification(Integer type) {
        if (type == null) {
            return "-";
        }
        return type == 1 ? "线上订单" : "线下订单";
    }
    private String formatOrderSource(Integer source) {
        if (source == null) {
            return "-";
        }
        if (source == 0) {
            return "自有平台";
        }
        if (source == 1) {
            return "第三方平台";
        }
        if (source == 2) {
            return "引流平台";
        }
        return "其他";
    }
    @RequiresPermissions(value = {"/invoiceManagement/select_order", "/paymentOrder/order"}, logical = Logical.OR)
    @ResponseBody
    @PostMapping(value = "/pay/order/list")
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/EnterpriseChargingAnalysisQuery.java
New file
@@ -0,0 +1,66 @@
package com.ruoyi.order.dto;
import com.ruoyi.common.core.web.page.BasePage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@ApiModel("Enterprise charging analysis query")
public class EnterpriseChargingAnalysisQuery extends BasePage {
    @ApiModelProperty("Company name")
    private String companyName;
    @ApiModelProperty("Social credit code")
    private String socialCreditCode;
    @ApiModelProperty("Exact company name")
    private String exactCompanyName;
    @ApiModelProperty("Site type")
    private Integer siteType;
    @ApiModelProperty("Partner id")
    private Integer partnerId;
    @ApiModelProperty("Site id")
    private Integer siteId;
    @ApiModelProperty("Order create start time")
    private LocalDateTime createTime1;
    @ApiModelProperty("Order create end time")
    private LocalDateTime createTime2;
    @ApiModelProperty("Order code")
    private String code;
    @ApiModelProperty("Charging order type")
    private Integer orderType;
    @ApiModelProperty("User phone")
    private String phone;
    @ApiModelProperty("Order status")
    private Integer status;
    @ApiModelProperty("Partner or service provider name")
    private String partnerName;
    @ApiModelProperty("Order source")
    private Integer orderSource;
    @ApiModelProperty("Charging start time begin")
    private LocalDateTime startTime1;
    @ApiModelProperty("Charging start time end")
    private LocalDateTime startTime2;
    @ApiModelProperty("Charging end time begin")
    private LocalDateTime endTime1;
    @ApiModelProperty("Charging end time end")
    private LocalDateTime endTime2;
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/EnterpriseChargingAnalysisVO.java
New file
@@ -0,0 +1,45 @@
package com.ruoyi.order.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@ApiModel("Enterprise charging analysis view")
public class EnterpriseChargingAnalysisVO {
    @ApiModelProperty("Company name")
    private String companyName;
    @ApiModelProperty("Social credit code")
    private String socialCreditCode;
    @ApiModelProperty("Company address")
    private String companyAddress;
    @ApiModelProperty("Company phone")
    private String companyPhone;
    @ApiModelProperty("Charging count")
    private Long chargingCount;
    @ApiModelProperty("Invoice count")
    private Long invoiceCount;
    @ApiModelProperty("Invoice amount")
    private BigDecimal invoiceAmount;
    @ApiModelProperty("Top site names")
    private String topSiteNames;
    @ApiModelProperty("Latest charging time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime latestChargingTime;
    @ApiModelProperty("Average charging interval hours")
    private BigDecimal averageChargingInterval;
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/EnterpriseChargingDetailVO.java
New file
@@ -0,0 +1,73 @@
package com.ruoyi.order.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@ApiModel("Enterprise charging detail view")
public class EnterpriseChargingDetailVO {
    private Long id;
    @ApiModelProperty("Order code")
    private String code;
    @ApiModelProperty("Charging order type")
    private Integer orderType;
    @ApiModelProperty("Order classification")
    private Integer orderClassification;
    @ApiModelProperty("Site name")
    private String siteName;
    @ApiModelProperty("Terminal name")
    private String terminalName;
    @ApiModelProperty("Status")
    private Integer status;
    @ApiModelProperty("Charging seconds")
    private Long chargingSecond;
    @ApiModelProperty("Total amount")
    private BigDecimal totalAmount;
    @ApiModelProperty("Coupon discount amount")
    private BigDecimal couponDiscountAmount;
    @ApiModelProperty("Vip discount amount")
    private BigDecimal vipDiscountAmount;
    @ApiModelProperty("Actual payment amount")
    private BigDecimal paymentAmount;
    @ApiModelProperty("Refund amount")
    private BigDecimal refundAmount;
    @ApiModelProperty("Order source")
    private Integer orderSource;
    @ApiModelProperty("Partner name")
    private String partnerName;
    @ApiModelProperty("Phone")
    private String phone;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime startTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime endTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime payTime;
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/EnterpriseChargingDetailExport.java
New file
@@ -0,0 +1,62 @@
package com.ruoyi.order.export;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
public class EnterpriseChargingDetailExport implements Serializable {
    @Excel(name = "订单编号", width = 30)
    private String code;
    @Excel(name = "订单类型", width = 20)
    private String orderType;
    @Excel(name = "订单分类", width = 20)
    private String orderClassification;
    @Excel(name = "站点名称", width = 30)
    private String siteName;
    @Excel(name = "充电终端名称", width = 30)
    private String terminalName;
    @Excel(name = "状态", width = 20)
    private String status;
    @Excel(name = "充电时长(秒)", width = 20)
    private Long chargingSecond;
    @Excel(name = "总金额", width = 20)
    private BigDecimal totalAmount;
    @Excel(name = "优惠券抵扣", width = 20)
    private BigDecimal couponDiscountAmount;
    @Excel(name = "会员抵扣", width = 20)
    private BigDecimal vipDiscountAmount;
    @Excel(name = "实付金额", width = 20)
    private BigDecimal paymentAmount;
    @Excel(name = "退款金额", width = 20)
    private BigDecimal refundAmount;
    @Excel(name = "归属合作商/服务商", width = 30)
    private String partnerName;
    @Excel(name = "手机号", width = 20)
    private String phone;
    @Excel(name = "订单来源", width = 20)
    private String orderSource;
    @Excel(name = "开始时间", width = 30)
    private String startTime;
    @Excel(name = "最后时间", width = 30)
    private String endTime;
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
@@ -67,6 +67,17 @@
    List<ChargingOrderBalanceIncomeDetailVO> exchangeCodeBalanceIncomeDetail(@Param("orderIds") List<Long> orderIds);
    List<PayOrderDto> payOrderQuery(@Param("pageInfo")PageInfo<PayOrderDto> pageInfo,@Param("data") PayOrderQueryDto payOrderQueryDto);
    List<EnterpriseChargingAnalysisVO> enterpriseChargingAnalysis(@Param("pageInfo") PageInfo<EnterpriseChargingAnalysisVO> pageInfo,
                                                                  @Param("data") EnterpriseChargingAnalysisQuery query);
    List<Map<String, Object>> enterpriseTopSites(@Param("data") EnterpriseChargingAnalysisQuery query);
    List<LocalDateTime> enterpriseChargingTimes(@Param("data") EnterpriseChargingAnalysisQuery query);
    List<EnterpriseChargingDetailVO> enterpriseChargingDetail(@Param("pageInfo") PageInfo<EnterpriseChargingDetailVO> pageInfo,
                                                              @Param("data") EnterpriseChargingAnalysisQuery query);
    List<EnterpriseChargingDetailVO> enterpriseChargingDetailExport(@Param("data") EnterpriseChargingAnalysisQuery query);
    List<TChargingOrderRefund> getRefundList(@Param("pageInfo")PageInfo<TChargingOrderRefund> pageInfo,@Param("data")  ChargingRefundDto chargingRefundDto);
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
@@ -147,6 +147,11 @@
    List<ChargingOrderBalanceIncomeDetailVO> balanceIncomeDetail(ChargingOrderQuery dto);
    R<PageInfo<PayOrderDto>> payOrderQuery(PayOrderQueryDto payOrderQueryDto);
    R<PageInfo<EnterpriseChargingAnalysisVO>> enterpriseChargingAnalysis(EnterpriseChargingAnalysisQuery query);
    R<PageInfo<EnterpriseChargingDetailVO>> enterpriseChargingDetail(EnterpriseChargingAnalysisQuery query);
    List<EnterpriseChargingDetailVO> enterpriseChargingDetailExport(EnterpriseChargingAnalysisQuery query);
    R<PageInfo<TChargingOrderRefund>> getRefundList(ChargingRefundDto chargingRefundDto);
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -2228,7 +2228,62 @@
        pageInfo.setRecords(list);
        return R.ok(pageInfo);
    }
        @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);
        for (EnterpriseChargingAnalysisVO row : list) {
            EnterpriseChargingAnalysisQuery rowQuery = buildEnterpriseRowQuery(query, row);
            List<Map<String, Object>> topSites = this.baseMapper.enterpriseTopSites(rowQuery);
            String topSiteNames = topSites.stream()
                    .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)));
        }
        pageInfo.setRecords(list);
        return R.ok(pageInfo);
    }
    @Override
    public R<PageInfo<EnterpriseChargingDetailVO>> enterpriseChargingDetail(EnterpriseChargingAnalysisQuery query) {
        PageInfo<EnterpriseChargingDetailVO> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
        List<EnterpriseChargingDetailVO> list = this.baseMapper.enterpriseChargingDetail(pageInfo, query);
        pageInfo.setRecords(list);
        return R.ok(pageInfo);
    }
    @Override
    public List<EnterpriseChargingDetailVO> enterpriseChargingDetailExport(EnterpriseChargingAnalysisQuery query) {
        return this.baseMapper.enterpriseChargingDetailExport(query);
    }
    private EnterpriseChargingAnalysisQuery buildEnterpriseRowQuery(EnterpriseChargingAnalysisQuery source, EnterpriseChargingAnalysisVO row) {
        EnterpriseChargingAnalysisQuery rowQuery = new EnterpriseChargingAnalysisQuery();
        BeanUtils.copyProperties(source, rowQuery);
        rowQuery.setCompanyName(null);
        rowQuery.setExactCompanyName(row.getCompanyName());
        rowQuery.setSocialCreditCode(row.getSocialCreditCode());
        return rowQuery;
    }
    private BigDecimal calculateAverageChargingInterval(List<LocalDateTime> times) {
        if (times == null || times.size() < 2) {
            return BigDecimal.ZERO;
        }
        long totalMinutes = 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;
            }
        }
        return BigDecimal.valueOf(totalMinutes)
                .divide(BigDecimal.valueOf(times.size() - 1L), 4, RoundingMode.HALF_UP)
                .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
    }
    @Override
    public R<PageInfo<TChargingOrderRefund>> getRefundList(ChargingRefundDto chargingRefundDto) {
        PageInfo<TChargingOrderRefund> pageInfo = new PageInfo<>(chargingRefundDto.getPageCurr(), chargingRefundDto.getPageSize());
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<!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">
@@ -2454,4 +2454,182 @@
            group by orderId,recharge_payment_type,site_id,recharge_amount,refund_amount,platformFee
        ) as aa group by aa.site_id,aa.recharge_payment_type order by aa.site_id
    </select>
    <sql id="enterpriseChargingBaseWhere">
        toi.invoicing_object_type = 2
        AND toi.status IN (2, 3)
        AND tod.order_type = 1
        AND co.del_flag = 0
        AND co.recharge_payment_status = 2
        <if test="data.exactCompanyName != null and data.exactCompanyName != ''">
            AND toi.name = #{data.exactCompanyName}
        </if>
        <if test="(data.exactCompanyName == null or data.exactCompanyName == '') and data.companyName != null and data.companyName != ''">
            AND toi.name LIKE CONCAT('%', #{data.companyName}, '%')
        </if>
        <if test="data.socialCreditCode != null and data.socialCreditCode != ''">
            AND IFNULL(toi.tax_identification_number, '') = #{data.socialCreditCode}
        </if>
        <if test="data.siteType != null">
            AND s.site_type = #{data.siteType}
        </if>
        <if test="data.partnerId != null">
            AND s.partner_id = #{data.partnerId}
        </if>
        <if test="data.siteId != null">
            AND co.site_id = #{data.siteId}
        </if>
        <if test="data.createTime1 != null">
            AND co.create_time &gt;= #{data.createTime1}
        </if>
        <if test="data.createTime2 != null">
            AND co.create_time &lt;= #{data.createTime2}
        </if>
    </sql>
    <select id="enterpriseChargingAnalysis" resultType="com.ruoyi.order.dto.EnterpriseChargingAnalysisVO">
        SELECT
            b.companyName,
            b.socialCreditCode,
            MAX(b.companyAddress) AS companyAddress,
            MAX(b.companyPhone) AS companyPhone,
            SUM(b.orderCount) AS chargingCount,
            COUNT(1) AS invoiceCount,
            SUM(CASE WHEN b.detailInvoiceAmount &gt; 0 THEN b.detailInvoiceAmount ELSE b.totalAmount END) AS invoiceAmount,
            MAX(b.latestChargingTime) AS latestChargingTime
        FROM (
            SELECT
                toi.id AS invoiceId,
                toi.name AS companyName,
                IFNULL(toi.tax_identification_number, '') AS socialCreditCode,
                MAX(toi.company_address) AS companyAddress,
                MAX(toi.company_phone) AS companyPhone,
                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
            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" />
            </where>
            GROUP BY toi.id, toi.name, toi.tax_identification_number
        ) b
        GROUP BY b.companyName, b.socialCreditCode
        ORDER BY chargingCount DESC
    </select>
    <select id="enterpriseTopSites" resultType="map">
        SELECT
            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
        </where>
        GROUP BY s.id, s.name
        ORDER BY counts DESC
        LIMIT 3
    </select>
    <select id="enterpriseChargingTimes" resultType="java.time.LocalDateTime">
        SELECT DISTINCT co.start_time
        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
        </where>
        ORDER BY co.start_time ASC
    </select>
    <sql id="enterpriseChargingDetailWhere">
        <include refid="enterpriseChargingBaseWhere" />
        <if test="data.code != null and data.code != ''">
            AND co.code LIKE CONCAT('%', #{data.code}, '%')
        </if>
        <if test="data.orderType != null">
            AND co.order_type = #{data.orderType}
        </if>
        <if test="data.phone != null and data.phone != ''">
            AND au.phone LIKE CONCAT('%', #{data.phone}, '%')
        </if>
        <if test="data.status != null">
            AND co.status = #{data.status}
        </if>
        <if test="data.partnerName != null and data.partnerName != ''">
            AND p.name LIKE CONCAT('%', #{data.partnerName}, '%')
        </if>
        <if test="data.orderSource != null">
            AND co.order_source = #{data.orderSource}
        </if>
        <if test="data.startTime1 != null">
            AND co.start_time &gt;= #{data.startTime1}
        </if>
        <if test="data.startTime2 != null">
            AND co.start_time &lt;= #{data.startTime2}
        </if>
        <if test="data.endTime1 != null">
            AND co.end_time &gt;= #{data.endTime1}
        </if>
        <if test="data.endTime2 != null">
            AND co.end_time &lt;= #{data.endTime2}
        </if>
    </sql>
    <sql id="enterpriseChargingDetailSelect">
        SELECT DISTINCT
            co.id,
            co.code,
            co.order_type AS orderType,
            co.order_classification AS orderClassification,
            s.name AS siteName,
            CONCAT(IFNULL(cp.name, ''), IFNULL(cg.name, '')) AS terminalName,
            co.status,
            (UNIX_TIMESTAMP(co.end_time) - UNIX_TIMESTAMP(co.start_time)) AS chargingSecond,
            co.recharge_amount AS totalAmount,
            IFNULL(co.coupon_discount_amount, 0) AS couponDiscountAmount,
            IFNULL(co.vip_discount_amount, 0) AS vipDiscountAmount,
            co.payment_amount AS paymentAmount,
            IFNULL(co.refund_amount, 0) AS refundAmount,
            co.order_source AS orderSource,
            p.name AS partnerName,
            au.phone AS phone,
            co.create_time AS createTime,
            co.start_time AS startTime,
            co.end_time AS endTime,
            co.pay_time AS payTime
        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
        LEFT JOIN charging_pile_service.t_partner p ON p.id = s.partner_id
        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
    </sql>
    <select id="enterpriseChargingDetail" resultType="com.ruoyi.order.dto.EnterpriseChargingDetailVO">
        <include refid="enterpriseChargingDetailSelect" />
        <where>
            <include refid="enterpriseChargingDetailWhere" />
        </where>
        ORDER BY co.create_time DESC
    </select>
    <select id="enterpriseChargingDetailExport" resultType="com.ruoyi.order.dto.EnterpriseChargingDetailVO">
        <include refid="enterpriseChargingDetailSelect" />
        <where>
            <include refid="enterpriseChargingDetailWhere" />
        </where>
        ORDER BY co.create_time DESC
    </select>
</mapper>