From 2dfbc3278649e8cd38afd9bdbc6a4ba736f15749 Mon Sep 17 00:00:00 2001 From: zhx Date: Thu, 9 Jul 2026 15:50:08 +0800 Subject: [PATCH] feat: add salary settlement records --- ...20260709_salary_settlement_record_menu.sql | 98 ++++++ ...20260709_salary_settlement_record_menu.sql | 98 ++++++ .../team/bd/SalarySettlementRecordClient.java | 15 + .../api/SalarySettlementRecordClientApi.java | 25 ++ .../bd/SalarySettlementRecordPageQryCmd.java | 51 +++ .../agency/bd/SalarySettlementRecordDTO.java | 127 +++++++ .../app/team/TeamSalaryRestController.java | 27 ++ .../app/team/TeamSalaryBackServiceImpl.java | 45 +++ .../team/SalarySettlementRecordCO.java | 27 ++ .../app/team/TeamSalaryBackService.java | 8 + .../SalarySettlementRecordClientEndpoint.java | 32 ++ .../SalarySettlementRecordClientService.java | 18 + ...larySettlementRecordClientServiceImpl.java | 310 ++++++++++++++++++ 13 files changed, 881 insertions(+) create mode 100644 .deploy/prod-deploy/sql/20260709_salary_settlement_record_menu.sql create mode 100644 .deploy/test-deploy/sql/20260709_salary_settlement_record_menu.sql create mode 100644 rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/bd/SalarySettlementRecordClient.java create mode 100644 rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/bd/api/SalarySettlementRecordClientApi.java create mode 100644 rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/team/bd/SalarySettlementRecordPageQryCmd.java create mode 100644 rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/agency/bd/SalarySettlementRecordDTO.java create mode 100644 rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/team/SalarySettlementRecordCO.java create mode 100644 rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/endpoint/team/bd/SalarySettlementRecordClientEndpoint.java create mode 100644 rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/SalarySettlementRecordClientService.java create mode 100644 rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/SalarySettlementRecordClientServiceImpl.java diff --git a/.deploy/prod-deploy/sql/20260709_salary_settlement_record_menu.sql b/.deploy/prod-deploy/sql/20260709_salary_settlement_record_menu.sql new file mode 100644 index 00000000..e3860527 --- /dev/null +++ b/.deploy/prod-deploy/sql/20260709_salary_settlement_record_menu.sql @@ -0,0 +1,98 @@ +-- Webconsole salary settlement records menu. +-- Adds the second-level menu under 主播中心 and binds the list API resource. + +INSERT INTO sys_resource (id, resource_name, mapping, method, auth_type, perm, update_time) +VALUES ( + 'team:salary:settlement:list', + '工资记录列表', + '/team/salary/settlement/list', + 'GET', + 3, + 'team:salary:record:list', + NOW() +) +ON DUPLICATE KEY UPDATE + resource_name = VALUES(resource_name), + mapping = VALUES(mapping), + method = VALUES(method), + auth_type = VALUES(auth_type), + perm = VALUES(perm), + update_time = NOW(); + +INSERT INTO sys_menu ( + parent_id, + menu_name, + path, + router, + menu_type, + icon, + alias, + sort, + status, + create_time, + update_time +) +SELECT + parent.id, + '工资记录', + 'team/salary-record/index', + 'team/salary-record', + 2, + 'form', + 'team:salary:record', + 880, + 0, + NOW(), + NOW() +FROM sys_menu parent +LEFT JOIN sys_menu existing ON existing.alias = 'team:salary:record' +WHERE (parent.alias = 'team:manaber' OR parent.router = '/team' OR parent.menu_name = '主播中心') + AND parent.menu_type = 1 + AND existing.id IS NULL +LIMIT 1; + +UPDATE sys_menu salary_menu +JOIN ( + SELECT id + FROM sys_menu + WHERE (alias = 'team:manaber' OR router = '/team' OR menu_name = '主播中心') + AND menu_type = 1 + LIMIT 1 +) parent ON 1 = 1 +SET + salary_menu.parent_id = parent.id, + salary_menu.menu_name = '工资记录', + salary_menu.path = 'team/salary-record/index', + salary_menu.router = 'team/salary-record', + salary_menu.menu_type = 2, + salary_menu.icon = 'form', + salary_menu.sort = 880, + salary_menu.status = 0, + salary_menu.update_time = NOW() +WHERE salary_menu.alias = 'team:salary:record'; + +INSERT INTO sys_menu_resource (menu_id, resource_id) +SELECT menu.id, resource.id +FROM sys_menu menu +JOIN sys_resource resource ON resource.id = 'team:salary:settlement:list' +LEFT JOIN sys_menu_resource existing + ON existing.menu_id = menu.id + AND existing.resource_id = resource.id +WHERE menu.alias = 'team:salary:record' + AND existing.id IS NULL; + +-- Salary records include admin salary, so auto-grant only to roles that can manage system permissions. +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT DISTINCT source_role.role_id, salary_menu.id +FROM sys_role_menu source_role +JOIN sys_menu source_menu ON source_menu.id = source_role.menu_id +JOIN sys_menu salary_menu ON salary_menu.alias = 'team:salary:record' +LEFT JOIN sys_role_menu existing + ON existing.role_id = source_role.role_id + AND existing.menu_id = salary_menu.id +WHERE source_menu.alias IN ( + 'sys:menu:list', + 'sys:role:list', + 'sys:resource:list' +) + AND existing.id IS NULL; diff --git a/.deploy/test-deploy/sql/20260709_salary_settlement_record_menu.sql b/.deploy/test-deploy/sql/20260709_salary_settlement_record_menu.sql new file mode 100644 index 00000000..e3860527 --- /dev/null +++ b/.deploy/test-deploy/sql/20260709_salary_settlement_record_menu.sql @@ -0,0 +1,98 @@ +-- Webconsole salary settlement records menu. +-- Adds the second-level menu under 主播中心 and binds the list API resource. + +INSERT INTO sys_resource (id, resource_name, mapping, method, auth_type, perm, update_time) +VALUES ( + 'team:salary:settlement:list', + '工资记录列表', + '/team/salary/settlement/list', + 'GET', + 3, + 'team:salary:record:list', + NOW() +) +ON DUPLICATE KEY UPDATE + resource_name = VALUES(resource_name), + mapping = VALUES(mapping), + method = VALUES(method), + auth_type = VALUES(auth_type), + perm = VALUES(perm), + update_time = NOW(); + +INSERT INTO sys_menu ( + parent_id, + menu_name, + path, + router, + menu_type, + icon, + alias, + sort, + status, + create_time, + update_time +) +SELECT + parent.id, + '工资记录', + 'team/salary-record/index', + 'team/salary-record', + 2, + 'form', + 'team:salary:record', + 880, + 0, + NOW(), + NOW() +FROM sys_menu parent +LEFT JOIN sys_menu existing ON existing.alias = 'team:salary:record' +WHERE (parent.alias = 'team:manaber' OR parent.router = '/team' OR parent.menu_name = '主播中心') + AND parent.menu_type = 1 + AND existing.id IS NULL +LIMIT 1; + +UPDATE sys_menu salary_menu +JOIN ( + SELECT id + FROM sys_menu + WHERE (alias = 'team:manaber' OR router = '/team' OR menu_name = '主播中心') + AND menu_type = 1 + LIMIT 1 +) parent ON 1 = 1 +SET + salary_menu.parent_id = parent.id, + salary_menu.menu_name = '工资记录', + salary_menu.path = 'team/salary-record/index', + salary_menu.router = 'team/salary-record', + salary_menu.menu_type = 2, + salary_menu.icon = 'form', + salary_menu.sort = 880, + salary_menu.status = 0, + salary_menu.update_time = NOW() +WHERE salary_menu.alias = 'team:salary:record'; + +INSERT INTO sys_menu_resource (menu_id, resource_id) +SELECT menu.id, resource.id +FROM sys_menu menu +JOIN sys_resource resource ON resource.id = 'team:salary:settlement:list' +LEFT JOIN sys_menu_resource existing + ON existing.menu_id = menu.id + AND existing.resource_id = resource.id +WHERE menu.alias = 'team:salary:record' + AND existing.id IS NULL; + +-- Salary records include admin salary, so auto-grant only to roles that can manage system permissions. +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT DISTINCT source_role.role_id, salary_menu.id +FROM sys_role_menu source_role +JOIN sys_menu source_menu ON source_menu.id = source_role.menu_id +JOIN sys_menu salary_menu ON salary_menu.alias = 'team:salary:record' +LEFT JOIN sys_role_menu existing + ON existing.role_id = source_role.role_id + AND existing.menu_id = salary_menu.id +WHERE source_menu.alias IN ( + 'sys:menu:list', + 'sys:role:list', + 'sys:resource:list' +) + AND existing.id IS NULL; diff --git a/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/bd/SalarySettlementRecordClient.java b/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/bd/SalarySettlementRecordClient.java new file mode 100644 index 00000000..25d507b1 --- /dev/null +++ b/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/bd/SalarySettlementRecordClient.java @@ -0,0 +1,15 @@ +package com.red.circle.other.inner.endpoint.team.bd; + +import com.red.circle.other.inner.endpoint.team.bd.api.SalarySettlementRecordClientApi; +import org.springframework.cloud.openfeign.FeignClient; + +/** + * BD/BD Leader/Admin 工资结算记录. + * + * @author AI Assistant + */ +@FeignClient(name = "salarySettlementRecordClient", url = "${feign.other.url}" + + SalarySettlementRecordClientApi.API_PREFIX) +public interface SalarySettlementRecordClient extends SalarySettlementRecordClientApi { + +} diff --git a/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/bd/api/SalarySettlementRecordClientApi.java b/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/bd/api/SalarySettlementRecordClientApi.java new file mode 100644 index 00000000..acc7406c --- /dev/null +++ b/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/bd/api/SalarySettlementRecordClientApi.java @@ -0,0 +1,25 @@ +package com.red.circle.other.inner.endpoint.team.bd.api; + +import com.red.circle.framework.dto.PageResult; +import com.red.circle.framework.dto.ResultResponse; +import com.red.circle.other.inner.model.cmd.team.bd.SalarySettlementRecordPageQryCmd; +import com.red.circle.other.inner.model.dto.agency.bd.SalarySettlementRecordDTO; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * BD/BD Leader/Admin 工资结算记录. + * + * @author AI Assistant + */ +public interface SalarySettlementRecordClientApi { + + String API_PREFIX = "/bd/salary-settlement-record/client"; + + /** + * 分页查询工资结算记录. + */ + @PostMapping("/page") + ResultResponse> page( + @RequestBody SalarySettlementRecordPageQryCmd qryCmd); +} diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/team/bd/SalarySettlementRecordPageQryCmd.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/team/bd/SalarySettlementRecordPageQryCmd.java new file mode 100644 index 00000000..7fc3c217 --- /dev/null +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/cmd/team/bd/SalarySettlementRecordPageQryCmd.java @@ -0,0 +1,51 @@ +package com.red.circle.other.inner.model.cmd.team.bd; + +import com.red.circle.framework.core.dto.PageCommand; +import java.io.Serial; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * BD/BD Leader/Admin 工资结算记录分页查询. + * + * @author AI Assistant + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class SalarySettlementRecordPageQryCmd extends PageCommand { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 来源系统. + */ + private String sysOrigin; + + /** + * 身份:BD | BD_LEADER | ADMIN,空表示全部. + */ + private String identity; + + /** + * 用户 ID. + */ + private Long userId; + + /** + * 账期,yyyyMMdd 半月起始日,如 20260701 / 20260716. + */ + private Integer billBelong; + + /** + * 状态:PENDING | SUCCESS | FAILED. + */ + private String status; + + /** + * 幂等业务号. + */ + private String bizNo; +} diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/agency/bd/SalarySettlementRecordDTO.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/agency/bd/SalarySettlementRecordDTO.java new file mode 100644 index 00000000..ad5ddf23 --- /dev/null +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/agency/bd/SalarySettlementRecordDTO.java @@ -0,0 +1,127 @@ +package com.red.circle.other.inner.model.dto.agency.bd; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.red.circle.framework.dto.DTO; +import java.io.Serial; +import java.math.BigDecimal; +import java.sql.Timestamp; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * BD/BD Leader/Admin 工资结算记录. + * + * @author AI Assistant + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +public class SalarySettlementRecordDTO extends DTO { + + @Serial + private static final long serialVersionUID = 1L; + + private String id; + + @JsonSerialize(using = ToStringSerializer.class) + private Long timeId; + + private String sysOrigin; + + /** + * 统一用户 ID,按身份分别对应 bdUserId/bdLeaderUserId/adminUserId. + */ + @JsonSerialize(using = ToStringSerializer.class) + private Long userId; + + private String identity; + + @JsonSerialize(using = ToStringSerializer.class) + private Long bdUserId; + + @JsonSerialize(using = ToStringSerializer.class) + private Long bdLeaderUserId; + + @JsonSerialize(using = ToStringSerializer.class) + private Long adminUserId; + + private Boolean isAdmin; + + private Integer billBelong; + + private String billTitle; + + private String bizNo; + + private String status; + + private String failureReason; + + private Integer agencyNumber; + + private Integer bdNumber; + + private BigDecimal teamSalaryAmount; + + private BigDecimal teamRechargeAmount; + + private String hitPolicyType; + + private Integer hitLevel; + + private BigDecimal commissionRate; + + private BigDecimal basicSalary; + + private BigDecimal rewardAmount; + + private Boolean rewardEligible; + + private Integer newRechargeAgentCount; + + private BigDecimal settlementSalary; + + private BigDecimal rechargeAgentAmount; + + private BigDecimal rechargeAgentSalary; + + private BigDecimal rechargeAgentPercentage; + + private BigDecimal bdLeaderTeamSalary; + + private BigDecimal bdLeaderSalary; + + private Integer bdLeaderHitLevel; + + private BigDecimal bdLeaderBasicSalary; + + private BigDecimal bdLeaderRewardAmount; + + private BigDecimal bdTeamSalaryAmount; + + private BigDecimal bdTeamSalarySalary; + + private Integer bdTeamSalaryHitLevel; + + private BigDecimal bdTeamSalaryCommissionRate; + + private BigDecimal bdTeamRechargeAmount; + + private BigDecimal bdTeamRechargeSalary; + + private Integer bdTeamRechargeHitLevel; + + private BigDecimal bdTeamRechargeCommissionRate; + + private BigDecimal bdPolicyFinalSalary; + + private String bdPolicyHitType; + + private BigDecimal totalSalary; + + private Timestamp createTime; + + private Timestamp updateTime; +} diff --git a/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/team/TeamSalaryRestController.java b/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/team/TeamSalaryRestController.java index 4e500d5b..f4f4ac43 100644 --- a/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/team/TeamSalaryRestController.java +++ b/rc-service/rc-service-console/console-adapter/src/main/java/com/red/circle/console/adapter/app/team/TeamSalaryRestController.java @@ -1,10 +1,14 @@ package com.red.circle.console.adapter.app.team; import com.red.circle.console.app.dto.clienobject.team.TeamSalaryCO; +import com.red.circle.console.app.dto.clienobject.team.SalarySettlementRecordCO; +import com.red.circle.console.app.service.admin.UserAccountService; import com.red.circle.console.app.service.app.team.TeamSalaryBackService; import com.red.circle.framework.dto.PageResult; import com.red.circle.framework.web.controller.BaseController; import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd; +import com.red.circle.other.inner.model.cmd.team.bd.SalarySettlementRecordPageQryCmd; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -20,6 +24,9 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("/team/salary") public class TeamSalaryRestController extends BaseController { + private static final String SALARY_RECORD_MENU_ALIAS = "team:salary:record"; + + private final UserAccountService userAccountService; private final TeamSalaryBackService teamSalaryBackService; /** @@ -30,4 +37,24 @@ public class TeamSalaryRestController extends BaseController { return teamSalaryBackService.listByCondition(query); } + /** + * BD/BD Leader/Admin 工资结算记录. + */ + @GetMapping("/settlement/list") + public PageResult listSettlementRecords( + SalarySettlementRecordPageQryCmd query) { + checkSalaryRecordPermission(); + query.setPageQuery(getPage()); + return teamSalaryBackService.listSettlementRecords(query); + } + + private void checkSalaryRecordPermission() { + Long reqUserId = getReqUserId(); + // 工资记录包含 Admin 薪资,不能只依赖菜单隐藏,接口层也必须校验显式菜单权限。 + if (Objects.isNull(reqUserId) + || !userAccountService.hasButtonsAlias(reqUserId.intValue(), SALARY_RECORD_MENU_ALIAS)) { + throw new RuntimeException("无权限访问工资记录"); + } + } + } diff --git a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/team/TeamSalaryBackServiceImpl.java b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/team/TeamSalaryBackServiceImpl.java index 85349501..75e9b140 100644 --- a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/team/TeamSalaryBackServiceImpl.java +++ b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/team/TeamSalaryBackServiceImpl.java @@ -3,14 +3,18 @@ package com.red.circle.console.app.service.app.team; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.red.circle.console.app.convertor.team.TeamAppConvertor; +import com.red.circle.console.app.dto.clienobject.team.SalarySettlementRecordCO; import com.red.circle.console.app.dto.clienobject.team.TeamSalaryCO; import com.red.circle.console.app.dto.clienobject.team.TeamSalaryDetailsCO; import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.framework.dto.PageResult; +import com.red.circle.other.inner.endpoint.team.bd.SalarySettlementRecordClient; import com.red.circle.other.inner.endpoint.team.target.TeamProfileClient; import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd; +import com.red.circle.other.inner.model.cmd.team.bd.SalarySettlementRecordPageQryCmd; import com.red.circle.other.inner.model.dto.agency.agency.TeamSalaryPaymentDTO; import com.red.circle.other.inner.model.dto.agency.agency.TeamSalaryPaymentDetailsDTO; +import com.red.circle.other.inner.model.dto.agency.bd.SalarySettlementRecordDTO; import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.text.StringUtils; import com.red.circle.other.inner.model.dto.user.UserProfileDTO; @@ -22,6 +26,7 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; /** @@ -37,6 +42,7 @@ public class TeamSalaryBackServiceImpl implements TeamSalaryBackService { private final TeamProfileClient teamProfileClient; private final UserProfileClient userProfileClient; private final RegionConfigClient regionConfigClient; + private final SalarySettlementRecordClient salarySettlementRecordClient; @Override public PageResult listByCondition(TeamSalaryBackQryCmd query) { @@ -113,6 +119,25 @@ public class TeamSalaryBackServiceImpl implements TeamSalaryBackService { return page; } + @Override + public PageResult listSettlementRecords( + SalarySettlementRecordPageQryCmd query) { + PageResult pageResult = ResponseAssert.requiredSuccess( + salarySettlementRecordClient.page(query)); + + PageResult page = pageResult.convert(this::toSettlementRecordCO); + if (pageResult.getTotal() <= 0) { + return page; + } + + Map userProfileMap = getSettlementUserProfileMap(pageResult); + page.setRecords(page.getRecords().stream().peek(record -> { + // 后台工资记录按身份统一展示,用户资料统一按结算用户 userId 补齐。 + record.setUserProfile(userProfileMap.get(record.getUserId())); + }).collect(Collectors.toList())); + return page; + } + private Map getMapRegionName(TeamSalaryBackQryCmd query) { return ResponseAssert.requiredSuccess( regionConfigClient.mapRegionNameBySysOrigin(query.getSysOrigin())); @@ -147,4 +172,24 @@ public class TeamSalaryBackServiceImpl implements TeamSalaryBackService { return ResponseAssert.requiredSuccess(userProfileClient.mapByUserIds(userIds)); } + private SalarySettlementRecordCO toSettlementRecordCO(SalarySettlementRecordDTO record) { + SalarySettlementRecordCO co = new SalarySettlementRecordCO(); + BeanUtils.copyProperties(record, co); + return co; + } + + private Map getSettlementUserProfileMap( + PageResult pageResult) { + Set userIds = pageResult.getRecords().stream() + .map(SalarySettlementRecordDTO::getUserId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (CollectionUtils.isEmpty(userIds)) { + return Maps.newHashMap(); + } + + return ResponseAssert.requiredSuccess(userProfileClient.mapByUserIds(userIds)); + } + } diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/team/SalarySettlementRecordCO.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/team/SalarySettlementRecordCO.java new file mode 100644 index 00000000..c4b6b8b8 --- /dev/null +++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/team/SalarySettlementRecordCO.java @@ -0,0 +1,27 @@ +package com.red.circle.console.app.dto.clienobject.team; + +import com.red.circle.other.inner.model.dto.agency.bd.SalarySettlementRecordDTO; +import com.red.circle.other.inner.model.dto.user.UserProfileDTO; +import java.io.Serial; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * BD/BD Leader/Admin 工资结算记录. + * + * @author AI Assistant + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class SalarySettlementRecordCO extends SalarySettlementRecordDTO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 结算用户资料. + */ + private UserProfileDTO userProfile; +} diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/team/TeamSalaryBackService.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/team/TeamSalaryBackService.java index 6f5f2918..37ce6a38 100644 --- a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/team/TeamSalaryBackService.java +++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/team/TeamSalaryBackService.java @@ -2,8 +2,10 @@ package com.red.circle.console.app.service.app.team; import com.red.circle.console.app.dto.clienobject.team.TeamSalaryCO; +import com.red.circle.console.app.dto.clienobject.team.SalarySettlementRecordCO; import com.red.circle.framework.dto.PageResult; import com.red.circle.other.inner.model.cmd.team.TeamSalaryBackQryCmd; +import com.red.circle.other.inner.model.cmd.team.bd.SalarySettlementRecordPageQryCmd; /** * 团队自动发送工资凭证. @@ -17,4 +19,10 @@ public interface TeamSalaryBackService { */ PageResult listByCondition(TeamSalaryBackQryCmd query); + /** + * 查询 BD/BD Leader/Admin 工资结算记录. + */ + PageResult listSettlementRecords( + SalarySettlementRecordPageQryCmd query); + } diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/endpoint/team/bd/SalarySettlementRecordClientEndpoint.java b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/endpoint/team/bd/SalarySettlementRecordClientEndpoint.java new file mode 100644 index 00000000..66a64c16 --- /dev/null +++ b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/endpoint/team/bd/SalarySettlementRecordClientEndpoint.java @@ -0,0 +1,32 @@ +package com.red.circle.other.app.inner.endpoint.team.bd; + +import com.red.circle.framework.dto.PageResult; +import com.red.circle.framework.dto.ResultResponse; +import com.red.circle.other.app.inner.service.team.SalarySettlementRecordClientService; +import com.red.circle.other.inner.endpoint.team.bd.api.SalarySettlementRecordClientApi; +import com.red.circle.other.inner.model.cmd.team.bd.SalarySettlementRecordPageQryCmd; +import com.red.circle.other.inner.model.dto.agency.bd.SalarySettlementRecordDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * BD/BD Leader/Admin 工资结算记录. + * + * @author AI Assistant + */ +@RestController +@RequestMapping(value = SalarySettlementRecordClientApi.API_PREFIX, + produces = MediaType.APPLICATION_JSON_VALUE) +@RequiredArgsConstructor +public class SalarySettlementRecordClientEndpoint implements SalarySettlementRecordClientApi { + + private final SalarySettlementRecordClientService salarySettlementRecordClientService; + + @Override + public ResultResponse> page( + SalarySettlementRecordPageQryCmd qryCmd) { + return ResultResponse.success(salarySettlementRecordClientService.page(qryCmd)); + } +} diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/SalarySettlementRecordClientService.java b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/SalarySettlementRecordClientService.java new file mode 100644 index 00000000..6fb856ec --- /dev/null +++ b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/SalarySettlementRecordClientService.java @@ -0,0 +1,18 @@ +package com.red.circle.other.app.inner.service.team; + +import com.red.circle.framework.dto.PageResult; +import com.red.circle.other.inner.model.cmd.team.bd.SalarySettlementRecordPageQryCmd; +import com.red.circle.other.inner.model.dto.agency.bd.SalarySettlementRecordDTO; + +/** + * BD/BD Leader/Admin 工资结算记录. + * + * @author AI Assistant + */ +public interface SalarySettlementRecordClientService { + + /** + * 分页查询工资结算记录. + */ + PageResult page(SalarySettlementRecordPageQryCmd qryCmd); +} diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/SalarySettlementRecordClientServiceImpl.java b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/SalarySettlementRecordClientServiceImpl.java new file mode 100644 index 00000000..e7026b8f --- /dev/null +++ b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/SalarySettlementRecordClientServiceImpl.java @@ -0,0 +1,310 @@ +package com.red.circle.other.app.inner.service.team.impl; + +import com.red.circle.framework.dto.PageQuery; +import com.red.circle.framework.dto.PageResult; +import com.red.circle.other.app.inner.service.team.SalarySettlementRecordClientService; +import com.red.circle.other.infra.database.mongo.entity.bd.AdminSalarySettlementRecord; +import com.red.circle.other.infra.database.mongo.entity.bd.BdLeaderSalarySettlementRecord; +import com.red.circle.other.infra.database.mongo.entity.bd.BdSalarySettlementRecord; +import com.red.circle.other.infra.enums.BdSettlementStatus; +import com.red.circle.other.inner.model.cmd.team.bd.SalarySettlementRecordPageQryCmd; +import com.red.circle.other.inner.model.dto.agency.bd.SalarySettlementRecordDTO; +import com.red.circle.tool.core.text.StringUtils; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Function; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Service; + +/** + * BD/BD Leader/Admin 工资结算记录. + * + * @author AI Assistant + */ +@Service +@RequiredArgsConstructor +public class SalarySettlementRecordClientServiceImpl implements SalarySettlementRecordClientService { + + private static final String IDENTITY_BD = "BD"; + private static final String IDENTITY_BD_LEADER = "BD_LEADER"; + private static final String IDENTITY_ADMIN = "ADMIN"; + private static final int DEFAULT_CURSOR = 1; + private static final int DEFAULT_LIMIT = 20; + + private final MongoTemplate mongoTemplate; + + @Override + public PageResult page(SalarySettlementRecordPageQryCmd qryCmd) { + SalarySettlementRecordPageQryCmd query = Objects.requireNonNullElseGet(qryCmd, + SalarySettlementRecordPageQryCmd::new); + PageBounds pageBounds = getPageBounds(query); + String identity = normalizeIdentity(query.getIdentity()); + if (StringUtils.isNotBlank(query.getIdentity()) && !isSupportedIdentity(identity)) { + return buildPage(Collections.emptyList(), 0, pageBounds); + } + + if (StringUtils.isNotBlank(identity)) { + return pageSingleIdentity(query, identity, pageBounds); + } + + List records = new ArrayList<>(); + long total = 0; + + total += count(query, "bdUserId", BdSalarySettlementRecord.class); + total += count(query, "bdLeaderUserId", BdLeaderSalarySettlementRecord.class); + total += count(query, "adminUserId", AdminSalarySettlementRecord.class); + records.addAll(listRecords(query, "bdUserId", BdSalarySettlementRecord.class, + this::toBdRecordDTO, 0, pageBounds.fetchSize())); + records.addAll(listRecords(query, "bdLeaderUserId", BdLeaderSalarySettlementRecord.class, + this::toBdLeaderRecordDTO, 0, pageBounds.fetchSize())); + records.addAll(listRecords(query, "adminUserId", AdminSalarySettlementRecord.class, + this::toAdminRecordDTO, 0, pageBounds.fetchSize())); + + // 三类记录分散在不同 collection,全部身份查询只各取当前页所需前 N 条再合并排序。 + records.sort((left, right) -> compareTimestampDesc(left.getCreateTime(), right.getCreateTime())); + Collection pageRecords = slice(records, pageBounds); + return buildPage(pageRecords, total, pageBounds); + } + + private PageResult pageSingleIdentity( + SalarySettlementRecordPageQryCmd query, String identity, PageBounds pageBounds) { + if (Objects.equals(identity, IDENTITY_BD)) { + return pageTypedRecords(query, "bdUserId", BdSalarySettlementRecord.class, + this::toBdRecordDTO, pageBounds); + } + if (Objects.equals(identity, IDENTITY_BD_LEADER)) { + return pageTypedRecords(query, "bdLeaderUserId", BdLeaderSalarySettlementRecord.class, + this::toBdLeaderRecordDTO, pageBounds); + } + return pageTypedRecords(query, "adminUserId", AdminSalarySettlementRecord.class, + this::toAdminRecordDTO, pageBounds); + } + + private PageResult pageTypedRecords( + SalarySettlementRecordPageQryCmd query, String userIdField, Class entityClass, + Function mapper, PageBounds pageBounds) { + long total = count(query, userIdField, entityClass); + List records = listRecords(query, userIdField, entityClass, mapper, + pageBounds.skip(), pageBounds.limit()); + return buildPage(records, total, pageBounds); + } + + private PageBounds getPageBounds(SalarySettlementRecordPageQryCmd query) { + PageQuery pageQuery = query.getPageQuery(); + int cursor = pageQuery == null || pageQuery.getCursor() == null ? DEFAULT_CURSOR + : pageQuery.getCursor(); + int limit = pageQuery == null || pageQuery.getLimit() == null ? DEFAULT_LIMIT + : pageQuery.getLimit(); + int safeCursor = cursor <= 0 ? DEFAULT_CURSOR : cursor; + int safeLimit = limit <= 0 ? DEFAULT_LIMIT : limit; + return new PageBounds(safeCursor, safeLimit); + } + + private Collection slice(List records, + PageBounds pageBounds) { + int fromIndex = Math.max(pageBounds.skip(), 0); + int toIndex = Math.min(fromIndex + pageBounds.limit(), records.size()); + return fromIndex >= records.size() + ? Collections.emptyList() + : records.subList(fromIndex, toIndex); + } + + private PageResult buildPage( + Collection records, long total, PageBounds pageBounds) { + PageResult pageResult = new PageResult<>(); + pageResult.setCurrent(pageBounds.cursor()); + pageResult.setSize(pageBounds.limit()); + pageResult.setTotal(total); + pageResult.setRecords(records); + return pageResult; + } + + private long count(SalarySettlementRecordPageQryCmd query, String userIdField, + Class entityClass) { + return mongoTemplate.count(buildQuery(query, userIdField), entityClass); + } + + private List listRecords(SalarySettlementRecordPageQryCmd query, + String userIdField, Class entityClass, Function mapper, + int skip, int limit) { + Query mongoQuery = buildQuery(query, userIdField); + mongoQuery.skip(skip); + mongoQuery.limit(limit); + return mongoTemplate.find(mongoQuery, entityClass) + .stream() + .map(mapper) + .toList(); + } + + private Query buildQuery(SalarySettlementRecordPageQryCmd query, String userIdField) { + List criteriaList = new ArrayList<>(); + if (StringUtils.isNotBlank(query.getSysOrigin())) { + criteriaList.add(Criteria.where("sysOrigin").is(query.getSysOrigin())); + } + if (Objects.nonNull(query.getUserId())) { + criteriaList.add(Criteria.where(userIdField).is(query.getUserId())); + } + if (Objects.nonNull(query.getBillBelong())) { + criteriaList.add(Criteria.where("billBelong").is(query.getBillBelong())); + } + if (StringUtils.isNotBlank(query.getStatus())) { + BdSettlementStatus status = parseStatus(query.getStatus()); + criteriaList.add(Objects.isNull(status) + ? Criteria.where("_id").is("__invalid_status__") + : Criteria.where("status").is(status)); + } + if (StringUtils.isNotBlank(query.getBizNo())) { + criteriaList.add(Criteria.where("bizNo").is(query.getBizNo())); + } + + Query mongoQuery = criteriaList.isEmpty() + ? new Query() + : new Query(new Criteria().andOperator(criteriaList.toArray(new Criteria[0]))); + mongoQuery.with(Sort.by(Sort.Order.desc("createTime"))); + return mongoQuery; + } + + private BdSettlementStatus parseStatus(String status) { + try { + return BdSettlementStatus.valueOf(status.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + return null; + } + } + + private String normalizeIdentity(String identity) { + return StringUtils.isBlank(identity) ? null : identity.trim().toUpperCase(Locale.ROOT); + } + + private boolean isSupportedIdentity(String identity) { + return Objects.equals(identity, IDENTITY_BD) + || Objects.equals(identity, IDENTITY_BD_LEADER) + || Objects.equals(identity, IDENTITY_ADMIN); + } + + private int compareTimestampDesc(Timestamp left, Timestamp right) { + if (Objects.isNull(left) && Objects.isNull(right)) { + return 0; + } + if (Objects.isNull(left)) { + return 1; + } + if (Objects.isNull(right)) { + return -1; + } + return right.compareTo(left); + } + + private SalarySettlementRecordDTO toBdRecordDTO(BdSalarySettlementRecord record) { + String status = record.getStatus() == null ? null : record.getStatus().name(); + String hitPolicyType = record.getHitPolicyType() == null ? null : record.getHitPolicyType().name(); + return new SalarySettlementRecordDTO() + .setId(record.getId()) + .setTimeId(record.getTimeId()) + .setSysOrigin(record.getSysOrigin()) + .setIdentity(IDENTITY_BD) + .setUserId(record.getBdUserId()) + .setBdUserId(record.getBdUserId()) + .setIsAdmin(record.getIsAdmin()) + .setBillBelong(record.getBillBelong()) + .setBillTitle(record.getBillTitle()) + .setAgencyNumber(record.getAgencyNumber()) + .setTeamSalaryAmount(record.getTeamSalaryAmount()) + .setTeamRechargeAmount(record.getTeamRechargeAmount()) + .setHitPolicyType(hitPolicyType) + .setHitLevel(record.getHitLevel()) + .setCommissionRate(record.getCommissionRate()) + .setSettlementSalary(record.getSettlementSalary()) + .setStatus(status) + .setFailureReason(record.getFailureReason()) + .setBizNo(record.getBizNo()) + .setCreateTime(record.getCreateTime()) + .setUpdateTime(record.getUpdateTime()); + } + + private SalarySettlementRecordDTO toBdLeaderRecordDTO(BdLeaderSalarySettlementRecord record) { + String status = record.getStatus() == null ? null : record.getStatus().name(); + return new SalarySettlementRecordDTO() + .setId(record.getId()) + .setTimeId(record.getTimeId()) + .setSysOrigin(record.getSysOrigin()) + .setIdentity(IDENTITY_BD_LEADER) + .setUserId(record.getBdLeaderUserId()) + .setBdLeaderUserId(record.getBdLeaderUserId()) + .setIsAdmin(record.getIsAdmin()) + .setBillBelong(record.getBillBelong()) + .setBillTitle(record.getBillTitle()) + .setBdNumber(record.getBdNumber()) + .setTeamSalaryAmount(record.getTeamSalaryAmount()) + .setHitLevel(record.getHitLevel()) + .setBasicSalary(record.getBasicSalary()) + .setRewardAmount(record.getRewardAmount()) + .setRewardEligible(record.getRewardEligible()) + .setNewRechargeAgentCount(record.getNewRechargeAgentCount()) + .setSettlementSalary(record.getSettlementSalary()) + .setStatus(status) + .setFailureReason(record.getFailureReason()) + .setBizNo(record.getBizNo()) + .setCreateTime(record.getCreateTime()) + .setUpdateTime(record.getUpdateTime()); + } + + private SalarySettlementRecordDTO toAdminRecordDTO(AdminSalarySettlementRecord record) { + String status = record.getStatus() == null ? null : record.getStatus().name(); + return new SalarySettlementRecordDTO() + .setId(record.getId()) + .setTimeId(record.getTimeId()) + .setSysOrigin(record.getSysOrigin()) + .setIdentity(IDENTITY_ADMIN) + .setUserId(record.getAdminUserId()) + .setAdminUserId(record.getAdminUserId()) + .setBillBelong(record.getBillBelong()) + .setBillTitle(record.getBillTitle()) + .setBizNo(record.getBizNo()) + .setRechargeAgentAmount(record.getRechargeAgentAmount()) + .setRechargeAgentSalary(record.getRechargeAgentSalary()) + .setRechargeAgentPercentage(record.getRechargeAgentPercentage()) + .setBdLeaderTeamSalary(record.getBdLeaderTeamSalary()) + .setBdLeaderSalary(record.getBdLeaderSalary()) + .setBdLeaderHitLevel(record.getBdLeaderHitLevel()) + .setBdLeaderBasicSalary(record.getBdLeaderBasicSalary()) + .setBdLeaderRewardAmount(record.getBdLeaderRewardAmount()) + .setBdTeamSalaryAmount(record.getBdTeamSalaryAmount()) + .setBdTeamSalarySalary(record.getBdTeamSalarySalary()) + .setBdTeamSalaryHitLevel(record.getBdTeamSalaryHitLevel()) + .setBdTeamSalaryCommissionRate(record.getBdTeamSalaryCommissionRate()) + .setBdTeamRechargeAmount(record.getBdTeamRechargeAmount()) + .setBdTeamRechargeSalary(record.getBdTeamRechargeSalary()) + .setBdTeamRechargeHitLevel(record.getBdTeamRechargeHitLevel()) + .setBdTeamRechargeCommissionRate(record.getBdTeamRechargeCommissionRate()) + .setBdPolicyFinalSalary(record.getBdPolicyFinalSalary()) + .setBdPolicyHitType(record.getBdPolicyHitType()) + .setTotalSalary(record.getTotalSalary()) + .setSettlementSalary(record.getTotalSalary()) + .setStatus(status) + .setFailureReason(record.getFailureReason()) + .setCreateTime(record.getCreateTime()) + .setUpdateTime(record.getUpdateTime()); + } + + private record PageBounds(int cursor, int limit) { + + private int skip() { + return Math.max((cursor - 1) * limit, 0); + } + + private int fetchSize() { + long size = (long) cursor * limit; + return size > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) size; + } + } +}