longz
2026-07-29 607e4ab169b003991ed4788c9dbe0bd1fefc69f3
各类修复
5个文件已添加
14个文件已修改
594 ■■■■■ 已修改文件
docs/superpowers/plans/2026-07-29-partner-type-role-sync.md 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/AccountListVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysRoleController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/PartnerTypeRoleService.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/test/java/com/ruoyi/system/service/PartnerTypeRoleServiceTest.java 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/mapper/SiteMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/SiteMapper.xml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/SiteServiceImplTest.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/AccountSettlementStatementMapperTest.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/task/AccountSettlementIncomeTest.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260728_account_settlement_v2.sql 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/superpowers/plans/2026-07-29-partner-type-role-sync.md
New file
@@ -0,0 +1,50 @@
# Partner Type Role Sync Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Ensure partner accounts automatically receive the enabled system role matching their partner type whenever user permissions are loaded.
**Architecture:** Add a focused system-service component that resolves the current partner type through the existing charging-pile Feign client and synchronizes only the three built-in type roles in `sys_user_role`. Call it before login/current-user permissions are calculated, then reload the user so its role collection and menu permissions reflect the synchronized database state.
**Tech Stack:** Java 8, Spring Boot, MyBatis-Plus, OpenFeign, JUnit 5, Mockito
## Global Constraints
- Partner type `1` maps to role key `independent_partner`.
- Partner type `2` maps to role key `site_owner`.
- Partner type `3` maps to role key `service_provider`.
- Preserve roles outside these three built-in type roles.
- Remove stale built-in type roles when the partner type changes.
- Do not modify unrelated existing workspace changes.
---
### Task 1: Partner type role synchronizer
**Files:**
- Create: `ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/PartnerTypeRoleService.java`
- Test: `ruoyi-modules/ruoyi-system/src/test/java/com/ruoyi/system/service/PartnerTypeRoleServiceTest.java`
**Interfaces:**
- Consumes: `SiteClient.getPartnerR(Integer)`, `ISysRoleService`, and `ISysUserRoleService`.
- Produces: `void synchronize(SysUser user)`.
- [x] **Step 1: Write failing tests for all three mappings, custom-role preservation, stale built-in-role removal, non-partner bypass, and missing-role failure.**
- [x] **Step 2: Run `mvn -pl ruoyi-modules/ruoyi-system -am -Dtest=PartnerTypeRoleServiceTest -Dsurefire.failIfNoSpecifiedTests=false test` and verify the new tests fail before implementation.**
- [x] **Step 3: Implement role-key mapping and idempotent `sys_user_role` synchronization.**
- [x] **Step 4: Run the focused test and verify it passes.**
### Task 2: Permission-loading integration
**Files:**
- Modify: `ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java`
- Test: `ruoyi-modules/ruoyi-system/src/test/java/com/ruoyi/system/service/PartnerTypeRoleServiceTest.java`
**Interfaces:**
- Consumes: `PartnerTypeRoleService.synchronize(SysUser)`.
- Produces: login and current-user responses whose roles and permissions include the matching partner-type role.
- [x] **Step 1: Invoke synchronization in both `/user/info/{username}` and `/user/getInfo` before permission calculation.**
- [x] **Step 2: Reload `SysUser` after synchronization so `SysUser.roles` is current.**
- [x] **Step 3: Run the focused test and compile the system module.**
- [x] **Step 4: Review the diff to confirm unrelated working-tree changes remain untouched.**
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/AccountListVO.java
@@ -1,9 +1,7 @@
package com.ruoyi.order.api.vo;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.order.api.model.TChargingBill;
import com.ruoyi.order.api.model.TChargingOrder;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
ruoyi-modules/ruoyi-system/pom.xml
@@ -145,6 +145,11 @@
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-api-chargingPile</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
@@ -177,4 +182,4 @@
        </resources>
    </build>
   
</project>
</project>
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysRoleController.java
@@ -339,8 +339,8 @@
    @ApiOperation(value = "编辑角色", tags = {"管理后台-角色管理"})
    public AjaxResult roleUpdate(@Validated @RequestBody RoleUpdateDto dto) {
        SysRole oldRole = roleService.selectRoleById(dto.getRoleId());
        if (isSystemBuiltinRole(oldRole)) {
            return AjaxResult.error("系统内置角色不可编辑");
        if (oldRole != null && oldRole.isAdmin()) {
            return AjaxResult.error("超级管理员角色不可编辑");
        }
        SysRole role = new SysRole();
        role.setRoleName(dto.getRoleName());
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java
@@ -72,6 +72,9 @@
    private ISysPermissionService permissionService;
    @Resource
    private PartnerTypeRoleService partnerTypeRoleService;
    @Resource
    private ISysConfigService configService;
    @Resource
@@ -505,6 +508,7 @@
        if (StringUtils.isNull(sysUser)) {
            return R.fail("用户名/密码错误");
        }
        sysUser = synchronizePartnerTypeRole(sysUser);
        // 角色集合
        Set<String> roles = permissionService.getRolePermission(sysUser);
        // 权限集合
@@ -539,6 +543,7 @@
    @GetMapping("/getInfo")
    public AjaxResult getInfo() {
        SysUser user = userService.selectUserById(tokenService.getLoginUser().getUserid());
        user = synchronizePartnerTypeRole(user);
        // 角色集合
        Set<String> roles = permissionService.getRolePermission(user);
        // 权限集合
@@ -550,6 +555,14 @@
        return ajax;
    }
    private SysUser synchronizePartnerTypeRole(SysUser user) {
        partnerTypeRoleService.synchronize(user);
        if (user != null && Integer.valueOf(2).equals(user.getRoleType()) && user.getObjectId() != null) {
            return userService.selectUserById(user.getUserId());
        }
        return user;
    }
    /**
     * 发送重置密码验证码
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/PartnerTypeRoleService.java
New file
@@ -0,0 +1,122 @@
package com.ruoyi.system.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
import com.ruoyi.chargingPile.api.model.Partner;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.system.api.domain.SysRole;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.domain.SysUserRole;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * Synchronizes the built-in role granted to a partner account from its partner type.
 */
@Service
public class PartnerTypeRoleService {
    static final String INDEPENDENT_PARTNER_ROLE_KEY = "independent_partner";
    static final String SITE_OWNER_ROLE_KEY = "site_owner";
    static final String SERVICE_PROVIDER_ROLE_KEY = "service_provider";
    private static final Set<String> PARTNER_ROLE_KEYS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
            INDEPENDENT_PARTNER_ROLE_KEY,
            SITE_OWNER_ROLE_KEY,
            SERVICE_PROVIDER_ROLE_KEY
    )));
    private static final Map<Integer, String> PARTNER_TYPE_ROLE_KEYS;
    static {
        Map<Integer, String> roleKeys = new HashMap<>();
        roleKeys.put(1, INDEPENDENT_PARTNER_ROLE_KEY);
        roleKeys.put(2, SITE_OWNER_ROLE_KEY);
        roleKeys.put(3, SERVICE_PROVIDER_ROLE_KEY);
        PARTNER_TYPE_ROLE_KEYS = Collections.unmodifiableMap(roleKeys);
    }
    private final SiteClient siteClient;
    private final ISysRoleService roleService;
    private final ISysUserRoleService userRoleService;
    public PartnerTypeRoleService(SiteClient siteClient,
                                  ISysRoleService roleService,
                                  ISysUserRoleService userRoleService) {
        this.siteClient = siteClient;
        this.roleService = roleService;
        this.userRoleService = userRoleService;
    }
    @Transactional(rollbackFor = Exception.class)
    public void synchronize(SysUser user) {
        if (!isPartnerAccount(user)) {
            return;
        }
        R<Partner> partnerResult = siteClient.getPartnerR(user.getObjectId());
        Partner partner = partnerResult == null ? null : partnerResult.getData();
        if (partner == null) {
            throw new ServiceException("当前账号关联的合作商不存在");
        }
        String targetRoleKey = PARTNER_TYPE_ROLE_KEYS.get(partner.getPartnerType());
        if (targetRoleKey == null) {
            throw new ServiceException("当前合作商类型无效");
        }
        List<SysRole> builtInRoles = roleService.list(Wrappers.lambdaQuery(SysRole.class)
                .in(SysRole::getRoleKey, PARTNER_ROLE_KEYS)
                .eq(SysRole::getDelFlag, "0")
                .eq(SysRole::getStatus, "0"));
        Map<String, SysRole> roleByKey = builtInRoles.stream()
                .collect(Collectors.toMap(SysRole::getRoleKey, role -> role, (first, ignored) -> first));
        SysRole targetRole = roleByKey.get(targetRoleKey);
        if (targetRole == null) {
            throw new ServiceException("系统角色【" + targetRoleKey + "】不存在或已停用,请先配置角色");
        }
        Set<Long> builtInRoleIds = builtInRoles.stream()
                .map(SysRole::getRoleId)
                .collect(Collectors.toSet());
        List<SysUserRole> existingRoles = userRoleService.list(Wrappers.lambdaQuery(SysUserRole.class)
                .eq(SysUserRole::getUserId, user.getUserId())
                .in(SysUserRole::getRoleId, builtInRoleIds));
        boolean hasTargetRole = false;
        for (SysUserRole existingRole : existingRoles) {
            Long existingRoleId = existingRole.getRoleId();
            if (targetRole.getRoleId().equals(existingRoleId)) {
                hasTargetRole = true;
            } else if (builtInRoleIds.contains(existingRoleId)) {
                userRoleService.remove(Wrappers.lambdaQuery(SysUserRole.class)
                        .eq(SysUserRole::getUserId, user.getUserId())
                        .eq(SysUserRole::getRoleId, existingRoleId));
            }
        }
        if (!hasTargetRole) {
            SysUserRole userRole = new SysUserRole();
            userRole.setUserId(user.getUserId());
            userRole.setRoleId(targetRole.getRoleId());
            userRoleService.save(userRole);
        }
    }
    private boolean isPartnerAccount(SysUser user) {
        return user != null
                && user.getUserId() != null
                && Integer.valueOf(2).equals(user.getRoleType())
                && user.getObjectId() != null;
    }
}
ruoyi-modules/ruoyi-system/src/test/java/com/ruoyi/system/service/PartnerTypeRoleServiceTest.java
New file
@@ -0,0 +1,140 @@
package com.ruoyi.system.service;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
import com.ruoyi.chargingPile.api.model.Partner;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.system.api.domain.SysRole;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.domain.SysUserRole;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentCaptor;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class PartnerTypeRoleServiceTest {
    @ParameterizedTest
    @CsvSource({
            "1, independent_partner, 11",
            "2, site_owner, 12",
            "3, service_provider, 13"
    })
    void addsRoleMatchingPartnerType(Integer partnerType, String expectedRoleKey, Long expectedRoleId) {
        Fixture fixture = new Fixture(partnerType);
        fixture.stubBuiltInRoles();
        when(fixture.userRoleService.list(any(Wrapper.class))).thenReturn(Collections.emptyList());
        fixture.service.synchronize(fixture.user);
        ArgumentCaptor<SysUserRole> captor = ArgumentCaptor.forClass(SysUserRole.class);
        verify(fixture.userRoleService).save(captor.capture());
        assertEquals(expectedRoleId, captor.getValue().getRoleId());
        assertEquals(expectedRoleKey, fixture.roleKey(expectedRoleId));
    }
    @Test
    void replacesStaleBuiltInRoleButKeepsUnrelatedCustomRole() {
        Fixture fixture = new Fixture(2);
        fixture.stubBuiltInRoles();
        SysUserRole oldTypeRole = userRole(100L, 11L);
        SysUserRole customRole = userRole(100L, 99L);
        when(fixture.userRoleService.list(any(Wrapper.class)))
                .thenReturn(Arrays.asList(oldTypeRole, customRole));
        fixture.service.synchronize(fixture.user);
        verify(fixture.userRoleService).remove(any(Wrapper.class));
        ArgumentCaptor<SysUserRole> captor = ArgumentCaptor.forClass(SysUserRole.class);
        verify(fixture.userRoleService).save(captor.capture());
        assertEquals(Long.valueOf(12L), captor.getValue().getRoleId());
    }
    @Test
    void doesNothingForPlatformUser() {
        Fixture fixture = new Fixture(1);
        fixture.user.setRoleType(1);
        fixture.service.synchronize(fixture.user);
        verify(fixture.siteClient, never()).getPartnerR(any());
        verify(fixture.roleService, never()).list(any(Wrapper.class));
        verify(fixture.userRoleService, never()).save(any(SysUserRole.class));
    }
    @Test
    void failsClearlyWhenConfiguredRoleIsMissing() {
        Fixture fixture = new Fixture(3);
        when(fixture.roleService.list(any(Wrapper.class)))
                .thenReturn(Arrays.asList(role(11L, "independent_partner"), role(12L, "site_owner")));
        ServiceException exception = assertThrows(ServiceException.class,
                () -> fixture.service.synchronize(fixture.user));
        assertEquals("系统角色【service_provider】不存在或已停用,请先配置角色", exception.getMessage());
        verify(fixture.userRoleService, never()).save(any(SysUserRole.class));
    }
    private static SysRole role(Long roleId, String roleKey) {
        SysRole role = new SysRole();
        role.setRoleId(roleId);
        role.setRoleKey(roleKey);
        return role;
    }
    private static SysUserRole userRole(Long userId, Long roleId) {
        SysUserRole userRole = new SysUserRole();
        userRole.setUserId(userId);
        userRole.setRoleId(roleId);
        return userRole;
    }
    private static class Fixture {
        private final SiteClient siteClient = mock(SiteClient.class);
        private final ISysRoleService roleService = mock(ISysRoleService.class);
        private final ISysUserRoleService userRoleService = mock(ISysUserRoleService.class);
        private final PartnerTypeRoleService service =
                new PartnerTypeRoleService(siteClient, roleService, userRoleService);
        private final SysUser user = new SysUser();
        private Fixture(Integer partnerType) {
            user.setUserId(100L);
            user.setRoleType(2);
            user.setObjectId(715);
            Partner partner = new Partner();
            partner.setId(715);
            partner.setPartnerType(partnerType);
            when(siteClient.getPartnerR(715)).thenReturn(R.ok(partner));
        }
        private void stubBuiltInRoles() {
            when(roleService.list(any(Wrapper.class))).thenReturn(Arrays.asList(
                    role(11L, "independent_partner"),
                    role(12L, "site_owner"),
                    role(13L, "service_provider")
            ));
        }
        private String roleKey(Long roleId) {
            if (Long.valueOf(11L).equals(roleId)) {
                return "independent_partner";
            }
            if (Long.valueOf(12L).equals(roleId)) {
                return "site_owner";
            }
            return "service_provider";
        }
    }
}
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/mapper/SiteMapper.java
@@ -32,6 +32,12 @@
    List<GetSiteListDTO> getSiteList(PageInfo<GetSiteListDTO> pageInfo, @Param("item") GetSiteList siteList, @Param("ids") Set<Integer> ids);
    /**
     * 获取下拉框使用的轻量站点列表。
     */
    List<GetSiteListDTO> getSiteOptions(@Param("ids") Set<Integer> ids,
                                        @Param("partnerId") Integer partnerId);
    /**
     * 获取站点分页列表
     * @return
     */
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/SiteServiceImpl.java
@@ -369,36 +369,24 @@
    @Override
    public PageInfo<GetSiteListDTO> getSiteList1(Long userId) {
        GetSiteList siteList = new GetSiteList();
        siteList.setPageCurr(1);
        siteList.setPageSize(9999);
        Set<Integer> ids = null;
        //校验合作商权限
        SysUser sysUser = sysUserClient.getSysUser(userId).getData();
        Integer roleType = sysUser.getRoleType();
        Integer objectId = sysUser.getObjectId();
        R<Integer> admin = sysUserClient.isAdmin(tokenService.getLoginUser().getUserid());
        Integer partnerId = null;
        //合作商
        if(roleType == 2){
            ids = partnerService.authSite(objectId, SiteMenu.SITE_LIST);
            partnerId = sysUser.getObjectId();
        }else{
            //非管理员需要根据角色和用户配置查询允许的站点数据
            R<Integer> admin = sysUserClient.isAdmin(userId);
            if(admin.getData()!=1){
                ids = getAssignedSiteIds(sysUser.getUserId());
                if (ids.isEmpty()) {
                    ids.add(-1);
                }
            }
        }
        PageInfo<GetSiteListDTO> pageInfo = new PageInfo<>(siteList.getPageCurr(), siteList.getPageSize());
        List<GetSiteListDTO> list = this.baseMapper.getSiteList(pageInfo, siteList, ids);
        for (GetSiteListDTO getSiteListDTO : list) {
            Integer siteId = getSiteListDTO.getId();
            getSiteListDTO.setAuthAccountingStrategy(roleType == 1 ? true : partnerService.authMenu(objectId, siteId, SiteMenu.SITE_ACCOUNTING_STRATEGY));
            //平台账号才有此权限
            getSiteListDTO.setAuthPartner(roleType == 1 ? true : false);
            getSiteListDTO.setAuthUpdate(roleType == 1 ? true : partnerService.authMenu(objectId, siteId, SiteMenu.SITE_UPDATE));
            getSiteListDTO.setAuthDelete(roleType == 1 ? true : partnerService.authMenu(objectId, siteId, SiteMenu.SITE_DELETE));
        }
        return pageInfo.setRecords(list);
        PageInfo<GetSiteListDTO> pageInfo = new PageInfo<>(1, 9999);
        return pageInfo.setRecords(this.baseMapper.getSiteOptions(ids, partnerId));
    }
    private Set<Integer> getAssignedSiteIds(Long userId) {
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/SiteMapper.xml
@@ -128,6 +128,33 @@
        order by a.sort desc, a.create_time desc
    </select>
    <select id="getSiteOptions" resultType="com.ruoyi.chargingPile.api.dto.GetSiteListDTO">
        select a.id,
               a.code,
               a.name,
               a.partner_id as partnerId,
               b.partner_type as partnerType,
               b.name as partnerName,
               a.venue_partner_id as venuePartnerId,
               vp.name as venuePartnerName,
               a.site_type as siteType,
               a.status
        from t_site a
        left join t_partner b on a.partner_id = b.id
        left join t_partner vp on a.venue_partner_id = vp.id
        where a.del_flag = 0
        <if test="null != partnerId">
            and (a.partner_id = #{partnerId} or a.venue_partner_id = #{partnerId})
        </if>
        <if test="null == partnerId and null != ids and ids.size() > 0">
            and a.id in
            <foreach collection="ids" item="siteId" open="(" separator="," close=")">
                #{siteId}
            </foreach>
        </if>
        order by a.sort desc, a.create_time desc
    </select>
    <select id="pageList" resultType="com.ruoyi.chargingPile.api.vo.SiteVO">
        select
        ts.id, ts.partner_id, ts.code, ts.`name`, ts.site_type, ts.business_category, ts.member_discount_flag as memberDiscountFlag, ts.status, ts.construction_site, ts.img_url,
ruoyi-service/ruoyi-chargingPile/src/test/java/com/ruoyi/chargingPile/service/impl/SiteServiceImplTest.java
New file
@@ -0,0 +1,49 @@
package com.ruoyi.chargingPile.service.impl;
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
import com.ruoyi.chargingPile.mapper.SiteMapper;
import com.ruoyi.chargingPile.service.IPartnerService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.feignClient.SysUserClient;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Collections;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class SiteServiceImplTest {
    @Test
    void partnerSiteListIncludesAllSitesRelatedToCurrentPartner() {
        SiteServiceImpl service = new SiteServiceImpl();
        SiteMapper mapper = mock(SiteMapper.class);
        IPartnerService partnerService = mock(IPartnerService.class);
        SysUserClient sysUserClient = mock(SysUserClient.class);
        ReflectionTestUtils.setField(service, "baseMapper", mapper);
        ReflectionTestUtils.setField(service, "partnerService", partnerService);
        ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient);
        SysUser sysUser = new SysUser();
        sysUser.setUserId(100L);
        sysUser.setRoleType(2);
        sysUser.setObjectId(715);
        when(sysUserClient.getSysUser(100L)).thenReturn(R.ok(sysUser));
        when(mapper.getSiteOptions(isNull(), eq(715)))
                .thenReturn(Collections.<GetSiteListDTO>emptyList());
        service.getSiteList1(100L);
        verify(mapper).getSiteOptions(isNull(), eq(715));
        verify(partnerService, never()).authSite(any(), any());
        verify(sysUserClient, never()).isAdmin(any());
    }
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
@@ -279,6 +279,6 @@
     * @param startTime
     * @param endTime
     */
    List<Map<String, Object>> getAccountSettlementStatement(@Param("startTime") String startTime, @Param("endTime") String endTime,
                                                            @Param("rechargePaymentType") Integer rechargePaymentType);
    List<Map<String, Object>> getAccountSettlementStatement(@Param("startTime") String startTime,
                                                            @Param("endTime") String endTime);
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
@@ -447,7 +447,7 @@
     * @param endTime
     * @return
     */
    List<Map<String, Object>> getAccountSettlementStatement(String startTime, String endTime, Integer rechargePaymentType);
    List<Map<String, Object>> getAccountSettlementStatement(String startTime, String endTime);
    /**
     * 手动处理退款数据
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -7206,8 +7206,8 @@
     * @return
     */
    @Override
    public List<Map<String, Object>> getAccountSettlementStatement(String startTime, String endTime, Integer rechargePaymentType) {
        return this.baseMapper.getAccountSettlementStatement(startTime, endTime, rechargePaymentType);
    public List<Map<String, Object>> getAccountSettlementStatement(String startTime, String endTime) {
        return this.baseMapper.getAccountSettlementStatement(startTime, endTime);
    }
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/task/TaskUtil.java
@@ -279,7 +279,7 @@
    @Scheduled(fixedRate = 1000 * 60)
    public void taskOneMinutes() {
        if(null != port && port == 5400){
        if (isScheduledTaskPort(port)) {
            List<TChargingOrder> upStatusChargingOrder = chargingOrderService.findUpStatusChargingOrder();
            log.info("定时任务执行,查询需要手动修改状态的订单数量:{}", upStatusChargingOrder.size());
            for (TChargingOrder order : upStatusChargingOrder) {
@@ -297,7 +297,7 @@
    //十分钟循环执行的定时任务
    @Scheduled(fixedRate = 1000 * 60 * 10)
    public void taskTenMinutes() {
        if(null != port && port == 5400){
        if (isScheduledTaskPort(port)) {
            List<TChargingOrder> failedStartupOrder = chargingOrderService.findFailedStartupOrder();
            log.info("定时任务执行,查询到启动失败的订单数量:{}", failedStartupOrder.size());
            for (TChargingOrder order : failedStartupOrder) {
@@ -424,7 +424,7 @@
    //定时任务添加结算账单汇总数据
    @Scheduled(cron = "0 30 0 * * ?")
    public void taskMonth() {
        if(null != port && port == 5400){
        if (isScheduledTaskPort(port)) {
            //推送三方引流平台数据
            pushOrderDrainagePlatform();
            //添加充电算账单汇总数据
@@ -437,7 +437,7 @@
    // 每个月最后一天23点执行的定时任务
    @Scheduled(cron = "0 0 23 * * ?")
    public void taskLastDay() {
        if(null != port && port == 5400){
        if (isScheduledTaskPort(port)) {
            try {
                int dayOfMonth = LocalDate.now().getDayOfMonth();
                int lastDayOfMonth = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth();
@@ -460,6 +460,10 @@
    public void onApplicationEvent(WebServerInitializedEvent event) {
        port = event.getWebServer().getPort();
        System.out.println("端口号:" + port);
    }
    static boolean isScheduledTaskPort(Integer serverPort) {
        return Integer.valueOf(5400).equals(serverPort) || Integer.valueOf(9400).equals(serverPort);
    }
@@ -1161,7 +1165,7 @@
                    accountSettlementStatement.setCode("JSD"+string+randomDigits);
                    //根据业务订单查询汇总数据
                    List<Map<String, Object>> mapList = chargingOrderService.getAccountSettlementStatement(startTime, endTime, i);
                    List<Map<String, Object>> mapList = chargingOrderService.getAccountSettlementStatement(startTime, endTime);
                    BigDecimal rechargeAmount = BigDecimal.ZERO;
                    BigDecimal refundAmount = BigDecimal.ZERO;
                    BigDecimal platformFee = BigDecimal.ZERO;
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
@@ -2710,33 +2710,47 @@
    <select id="getAccountSettlementStatement" resultType="map">
        select
        aa.site_id as siteId,
        sum(aa.recharge_amount) as rechargeAmount,
        sum(aa.refund_amount) as refundAmount,
        sum(aa.serviceAmount) as serviceAmount
        from (
            select
            b.id as orderId,
            b.site_id,
            ifnull(b.payment_amount, 0) as recharge_amount,
            ifnull(c.refund_amount, 0) as refund_amount,
            sum(ifnull(a.period_service_price, 0)) as serviceAmount
            from t_charging_order_accounting_strategy a
            left join t_charging_order b on (a.charging_order_id = b.id)
            left join (
                select charging_order_id, sum(ifnull(refund_amount, 0)) as refund_amount
                from t_charging_order_refund
                where refund_status = 2
                group by charging_order_id
            ) c on b.id = c.charging_order_id
            where b.`status` = 5
              and b.del_flag = 0
              and b.order_source = 1
              and b.source_name = 'ICBC'
              and b.start_time between #{startTime} and #{endTime}
            group by orderId, site_id, recharge_amount, refund_amount
        ) as aa group by aa.site_id order by aa.site_id
        SELECT
            b.site_id AS siteId,
            SUM(CASE
                    WHEN sr.charging_order_id IS NOT NULL THEN sr.paid_amount
                    ELSE IFNULL(b.payment_amount, 0)
                END) AS rechargeAmount,
            SUM(IFNULL(r.refund_amount, 0)) AS refundAmount,
            SUM(CASE
                    WHEN sr.charging_order_id IS NOT NULL THEN sr.paid_service_amount
                    ELSE IFNULL(a.service_amount, 0)
                END) AS serviceAmount
        FROM t_charging_order b
        LEFT JOIN (
            SELECT charging_order_id,
                   SUM(IFNULL(period_service_price, 0)) AS service_amount
            FROM t_charging_order_accounting_strategy
            GROUP BY charging_order_id
        ) a ON a.charging_order_id = b.id
        LEFT JOIN (
            SELECT charging_order_id,
                   SUM(IFNULL(channel_pay_amount, 0)) AS paid_amount,
                   SUM(IFNULL(channel_service_amount, 0)) AS paid_service_amount
            FROM t_charging_order_share_record
            WHERE pay_channel = 'ICBC'
              AND del_flag = 0
            GROUP BY charging_order_id
        ) sr ON sr.charging_order_id = b.id
        LEFT JOIN (
            SELECT charging_order_id,
                   SUM(IFNULL(refund_amount, 0)) AS refund_amount
            FROM t_charging_order_refund
            WHERE refund_status = 2
            GROUP BY charging_order_id
        ) r ON r.charging_order_id = b.id
        WHERE b.status = 5
          AND b.del_flag = 0
          AND b.order_source = 1
          AND b.source_name = 'ICBC'
          AND b.start_time BETWEEN #{startTime} AND #{endTime}
        GROUP BY b.site_id
        ORDER BY b.site_id
    </select>
    <sql id="enterpriseChargingBaseWhere">
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/AccountSettlementStatementMapperTest.java
New file
@@ -0,0 +1,45 @@
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 AccountSettlementStatementMapperTest {
    @Test
    void usesIcbcPaidAmountsAndKeepsLegacyFieldsForCompatibility() throws IOException {
        String statementMapper = resource("mapper/order/TAccountSettlementStatementMapper.xml");
        assertTrue(statementMapper.contains("AND account_type = 3"));
        assertTrue(statementMapper.contains("SUM(platform_income)"));
        assertTrue(statementMapper.contains("SUM(partner_income)"));
        assertTrue(statementMapper.contains("platform_fee"));
        assertTrue(statementMapper.contains("tripartite_commission"));
        assertTrue(statementMapper.contains("recorded_amount"));
        String orderMapper = resource("mapper/order/TChargingOrderMapper.xml");
        String settlementQuery = orderMapper.substring(
                orderMapper.indexOf("<select id=\"getAccountSettlementStatement\""),
                orderMapper.indexOf("<sql id=\"enterpriseChargingBaseWhere\""));
        assertTrue(settlementQuery.contains("b.order_source = 1"));
        assertTrue(settlementQuery.contains("b.source_name = 'ICBC'"));
        assertTrue(settlementQuery.contains("pay_channel = 'ICBC'"));
        assertTrue(settlementQuery.contains("channel_pay_amount"));
        assertTrue(settlementQuery.contains("channel_service_amount"));
    }
    private String resource(String path) throws IOException {
        InputStream input = getClass().getClassLoader().getResourceAsStream(path);
        assertNotNull(input);
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
            return reader.lines().collect(Collectors.joining("\n"));
        }
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/util/task/AccountSettlementIncomeTest.java
@@ -9,6 +9,14 @@
class AccountSettlementIncomeTest {
    @Test
    void enablesScheduledTasksOnLegacyAndOrderPorts() {
        assertEquals(true, TaskUtil.isScheduledTaskPort(5400));
        assertEquals(true, TaskUtil.isScheduledTaskPort(9400));
        assertEquals(false, TaskUtil.isScheduledTaskPort(9500));
        assertEquals(false, TaskUtil.isScheduledTaskPort(null));
    }
    @Test
    void calculatesPartnerAndPlatformIncomeForMiniProgramOrder() {
        BigDecimal partnerIncome = TaskUtil.calculateAccountPartnerIncome(
                new BigDecimal("10.00"), new BigDecimal("30"), false);
sql/20260728_account_settlement_v2.sql
@@ -1,4 +1,3 @@
ALTER TABLE `t_account_settlement_statement`
    ADD COLUMN `platform_income` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '平台收入' AFTER `recorded_amount`,
    ADD COLUMN `partner_income` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '合作商收入' AFTER `platform_income`;