✨ 新增应收账单按角色和客户归属的行级数据权限
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# 应收账单行级权限
|
||||
|
||||
规则独立于 `@DataScope` 和角色管理的 `dataScope`,现有菜单、按钮权限保持不变。
|
||||
|
||||
| roleKey | 数据范围 |
|
||||
| --- | --- |
|
||||
| ORA_6041 / ORA_6163 | 销售员、商务专员:本人负责客户 |
|
||||
| ORA_6040 | 本部门人员负责客户,不含下级部门 |
|
||||
| ORA_6042 / ORA_6043 | 财务经理、应收财务:全部账单 |
|
||||
|
||||
超级管理员沿用系统判断。仅参与当前入口功能授权的有效角色合并范围;未知角色、空权限对象默认无数据。财务本期不使用账单 `create_user_id`。应付财务不参与应收账单权限规则。
|
||||
|
||||
归属链为 `ar_bill.customer_id → ctr_customer.sales_user_id → sys_user.user_id / dept_id`。销售员和商务专员均通过客户的 `sales_user_id` 与当前登录用户 ID 匹配本人客户。客户转交后,历史账单跟随当前归属变化。全量角色可见未关联销售、销售已删除、客户已删除的历史账单;受限角色不放行无有效归属的数据。业务员仅停用但未删除时,其部门经理仍可查看。
|
||||
|
||||
核心实现位于 `bms-system/src/main/java/com/bms/ar/security` 和 `mapper/ar/ArBillScopeMapper.xml`。内部权限对象作为独立 Mapper 参数传递,不绑定 HTTP 查询对象;后台生成和导出任务继续使用独立的内部读取方法。
|
||||
|
||||
接入范围包括列表、汇总、列表导出、详情/费用、日志、生成预览及提交、生成任务、导出任务及下载、客户账单入口,以及客户/收款页面的待核销账单、核销记录、核销和反核销。应收筛选及生成客户选项分别使用 `/ar/bill/customer-options`、`/ar/bill/generate/customer-options`。
|
||||
|
||||
用户编辑、调部门、停用、分配角色,以及角色编辑、停用、删除和授权关系调整,会在事务提交后让受影响的登录会话失效;相关用户需要重新登录。不改变角色配置本身。
|
||||
|
||||
## 本地验证
|
||||
|
||||
在 `bms-back` 下执行(PowerShell):
|
||||
|
||||
```powershell
|
||||
mvn -o -pl bms-admin -am -DskipTests compile
|
||||
```
|
||||
|
||||
本次新增的行级权限测试用例及其辅助脚本已按要求删除。
|
||||
|
||||
在 `bms-front` 下执行 `npm run build:prod` 验证前端构建。
|
||||
|
||||
## 部署前核实
|
||||
|
||||
- 本次没有新增数据库字段、权限配置表或迁移 SQL。
|
||||
- 目标环境 `ctr_customer.sales_user_id` 必须已经统一为系统用户 ID,不能仍是旧业务员工号。现有迁移文件位于 `sql/customer+supplier/20261028_customer_sales_user_migration.sql`,需要按原有流程确认执行状态,本次不自动执行。
|
||||
- 使用真实销售 A/B、经理、财务和商务专员账号检查数据范围与功能授权,特别是生产角色实际拥有的接口权限。
|
||||
- 上线前仍需在 MySQL 5.7 环境联调,并检查实际数据的执行计划。
|
||||
@@ -35,6 +35,7 @@ import com.bms.ar.domain.vo.ArBillGenerateRequest;
|
||||
import com.bms.ar.domain.vo.ArBillListExportRow;
|
||||
import com.bms.ar.domain.vo.ArBillQuery;
|
||||
import com.bms.ar.service.IArBillService;
|
||||
import com.bms.ar.security.ArBillScopeResolver;
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
@@ -59,6 +60,24 @@ public class ArBillController extends BaseController {
|
||||
@Autowired
|
||||
private IArBillService billService;
|
||||
|
||||
@Autowired
|
||||
private ArBillScopeResolver billScope;
|
||||
|
||||
/** 应收筛选专用客户选项,与账单使用同一角色规则。 */
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:list')")
|
||||
@GetMapping("/customer-options")
|
||||
public TableDataInfo customerOptions(@RequestParam(required = false) String keywords) {
|
||||
startPage();
|
||||
return getDataTable(billScope.selectCustomerOptions(keywords));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||
@GetMapping("/generate/customer-options")
|
||||
public TableDataInfo generateCustomerOptions(@RequestParam(required = false) String keywords) {
|
||||
startPage();
|
||||
return getDataTable(billScope.selectCustomerOptions(keywords));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数中的日期必须严格按 yyyy-MM-dd 解析,禁止非法日期自动滚动到其他日期。
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.bms.common.utils;
|
||||
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
/** 由服务端功能鉴权写入的请求上下文,不读取请求参数。 */
|
||||
public final class PermissionContext {
|
||||
private static final String ATTRIBUTE = "PERMISSION_CONTEXT";
|
||||
|
||||
private PermissionContext() {}
|
||||
|
||||
public static void set(String permission) {
|
||||
RequestContextHolder.currentRequestAttributes().setAttribute(ATTRIBUTE, permission, RequestAttributes.SCOPE_REQUEST);
|
||||
}
|
||||
|
||||
public static String get() {
|
||||
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
|
||||
Object value = attributes == null ? null : attributes.getAttribute(ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
|
||||
return value instanceof String ? (String) value : null;
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.bms.framework.aspectj;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import com.bms.common.core.domain.entity.SysRole;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.framework.web.service.TokenService;
|
||||
import com.bms.system.domain.SysUserRole;
|
||||
|
||||
/** 授权关系修改提交后,让旧登录态失效;不参与业务 SQL 数据范围计算。 */
|
||||
@Aspect
|
||||
@Component
|
||||
@Order(1)
|
||||
public class AuthorizationSessionAspect {
|
||||
private final TokenService tokenService;
|
||||
|
||||
public AuthorizationSessionAspect(TokenService tokenService) {
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
@AfterReturning(pointcut = "execution(* com.bms.system.service.impl.SysUserServiceImpl.updateUser(..))"
|
||||
+ " || execution(* com.bms.system.service.impl.SysUserServiceImpl.updateUserStatus(..))"
|
||||
+ " || execution(* com.bms.system.service.impl.SysUserServiceImpl.insertUserAuth(..))"
|
||||
+ " || execution(* com.bms.system.service.impl.SysUserServiceImpl.deleteUserBy*(..))", returning = "result")
|
||||
public void userChanged(JoinPoint point, Object result) {
|
||||
if (result instanceof Number && ((Number) result).intValue() <= 0) return;
|
||||
Object target = point.getArgs()[0];
|
||||
Set<Long> users = new HashSet<>();
|
||||
if (target instanceof SysUser) users.add(((SysUser) target).getUserId());
|
||||
else if (target instanceof Long[]) users.addAll(Arrays.asList((Long[]) target));
|
||||
else if (target instanceof Long) users.add((Long) target);
|
||||
invalidate(users, Set.of());
|
||||
}
|
||||
|
||||
@AfterReturning(pointcut = "execution(* com.bms.system.service.impl.SysRoleServiceImpl.updateRole(..))"
|
||||
+ " || execution(* com.bms.system.service.impl.SysRoleServiceImpl.updateRoleStatus(..))"
|
||||
+ " || execution(* com.bms.system.service.impl.SysRoleServiceImpl.deleteRoleBy*(..))"
|
||||
+ " || execution(* com.bms.system.service.impl.SysRoleServiceImpl.deleteAuthUser*(..))"
|
||||
+ " || execution(* com.bms.system.service.impl.SysRoleServiceImpl.insertAuthUsers(..))", returning = "result")
|
||||
public void roleChanged(JoinPoint point, Object result) {
|
||||
if (result instanceof Number && ((Number) result).intValue() <= 0) return;
|
||||
Object target = point.getArgs()[0];
|
||||
Set<Long> users = new HashSet<>();
|
||||
Set<Long> roles = new HashSet<>();
|
||||
if (target instanceof SysUserRole) users.add(((SysUserRole) target).getUserId());
|
||||
else if (point.getArgs().length > 1 && point.getArgs()[1] instanceof Long[])
|
||||
users.addAll(Arrays.asList((Long[]) point.getArgs()[1]));
|
||||
else if (target instanceof SysRole) roles.add(((SysRole) target).getRoleId());
|
||||
else if (target instanceof Long[]) roles.addAll(Arrays.asList((Long[]) target));
|
||||
else if (target instanceof Long) roles.add((Long) target);
|
||||
invalidate(users, roles);
|
||||
}
|
||||
|
||||
private void invalidate(Set<Long> users, Set<Long> roles) {
|
||||
users.remove(null);
|
||||
Set<Long> roleIds = new HashSet<>(roles);
|
||||
roleIds.remove(null);
|
||||
Set<Long> userIds = Set.copyOf(users);
|
||||
if (userIds.isEmpty() && roleIds.isEmpty()) return;
|
||||
Runnable action = () -> tokenService.evictAuthorizationSessions(userIds, roleIds);
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() { action.run(); }
|
||||
});
|
||||
} else {
|
||||
action.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-9
@@ -1,8 +1,6 @@
|
||||
package com.bms.framework.security.context;
|
||||
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import com.bms.common.core.text.Convert;
|
||||
import com.bms.common.utils.PermissionContext;
|
||||
|
||||
/**
|
||||
* 权限信息
|
||||
@@ -11,17 +9,13 @@ import com.bms.common.core.text.Convert;
|
||||
*/
|
||||
public class PermissionContextHolder
|
||||
{
|
||||
private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT";
|
||||
|
||||
public static void setContext(String permission)
|
||||
{
|
||||
RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission,
|
||||
RequestAttributes.SCOPE_REQUEST);
|
||||
PermissionContext.set(permission);
|
||||
}
|
||||
|
||||
public static String getContext()
|
||||
{
|
||||
return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES,
|
||||
RequestAttributes.SCOPE_REQUEST));
|
||||
return PermissionContext.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.bms.framework.web.service;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -231,6 +232,22 @@ public class TokenService
|
||||
return CacheConstants.LOGIN_TOKEN_KEY + uuid;
|
||||
}
|
||||
|
||||
/** 授权或部门变更后失效受影响会话,避免缓存中的旧角色继续扩大业务范围。 */
|
||||
public void evictAuthorizationSessions(Set<Long> userIds, Set<Long> roleIds)
|
||||
{
|
||||
Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
|
||||
if (keys == null) return;
|
||||
for (String key : keys)
|
||||
{
|
||||
LoginUser user = redisCache.getCacheObject(key);
|
||||
if (user == null || user.getUser() == null) continue;
|
||||
boolean affected = userIds.contains(user.getUserId())
|
||||
|| (user.getUser().getRoles() != null && user.getUser().getRoles().stream()
|
||||
.anyMatch(role -> role != null && roleIds.contains(role.getRoleId())));
|
||||
if (affected) redisCache.deleteObject(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色权限变更后,刷新所有持有该角色的在线用户权限
|
||||
*
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.bms.ar.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.bms.ar.security.ArBillRowScope;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.ar.domain.ArBalanceWriteoff;
|
||||
|
||||
@@ -33,5 +35,5 @@ public interface ArBalanceWriteoffMapper
|
||||
@Param("effectiveStatus") String effectiveStatus);
|
||||
|
||||
/** 查询核销记录列表(关联账单名称/客户名称) */
|
||||
List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query);
|
||||
List<ArBalanceWriteoff> selectWriteoffList(@Param("query") ArBalanceWriteoff query, @Param("scope") ArBillRowScope scope);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.bms.ar.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.bms.ar.security.ArBillRowScope;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.ar.domain.ArBillDetailFee;
|
||||
|
||||
public interface ArBillDetailFeeMapper {
|
||||
List<ArBillDetailFee> selectByBillIdAndDetailIdScoped(@Param("arBillId") Long arBillId, @Param("detailId") Long detailId, @Param("scope") ArBillRowScope scope);
|
||||
|
||||
List<ArBillDetailFee> selectByDetailId(Long detailId);
|
||||
|
||||
List<ArBillDetailFee> selectByBillIdAndDetailId(@Param("arBillId") Long arBillId,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package com.bms.ar.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.bms.ar.security.ArBillRowScope;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.ar.domain.ArBillDetail;
|
||||
|
||||
public interface ArBillDetailMapper {
|
||||
List<ArBillDetail> selectByBillIdScoped(@Param("arBillId") Long arBillId, @Param("scope") ArBillRowScope scope);
|
||||
|
||||
List<ArBillDetail> selectByBillId(Long arBillId);
|
||||
|
||||
List<ArBillDetail> selectByBillIdAndSourceIds(@Param("arBillId") Long arBillId,
|
||||
|
||||
@@ -10,6 +10,9 @@ import com.bms.ar.domain.ArBillGenerateTask;
|
||||
* 应收账单异步生成任务 Mapper。
|
||||
*/
|
||||
public interface ArBillGenerateTaskMapper {
|
||||
List<ArBillGenerateTask> selectActiveByCreateByScoped(@Param("createBy") String createBy,
|
||||
@Param("scope") com.bms.ar.security.ArBillRowScope scope);
|
||||
|
||||
ArBillGenerateTask selectById(Long taskId);
|
||||
|
||||
ArBillGenerateTask selectByRequestToken(String requestToken);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.bms.ar.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.bms.ar.security.ArBillRowScope;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.ar.domain.ArBillLog;
|
||||
|
||||
public interface ArBillLogMapper
|
||||
{
|
||||
List<ArBillLog> selectList(ArBillLog query);
|
||||
List<ArBillLog> selectList(@Param("query") ArBillLog query, @Param("scope") ArBillRowScope scope);
|
||||
|
||||
ArBillLog selectSuccessByRequestId(@Param("requestId") String requestId,
|
||||
@Param("objectType") String objectType,
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.bms.ar.mapper;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.bms.ar.security.ArBillRowScope;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.ar.domain.ArBill;
|
||||
@@ -10,12 +11,14 @@ import com.bms.ar.domain.vo.ArBillQuery;
|
||||
import com.bms.ar.domain.vo.ArBillSummary;
|
||||
|
||||
public interface ArBillMapper {
|
||||
List<ArBill> selectArBillList(ArBillQuery query);
|
||||
List<ArBill> selectArBillList(@Param("query") ArBillQuery query, @Param("scope") ArBillRowScope scope);
|
||||
|
||||
ArBillSummary selectArBillSummary(ArBillQuery query);
|
||||
ArBillSummary selectArBillSummary(@Param("query") ArBillQuery query, @Param("scope") ArBillRowScope scope);
|
||||
|
||||
ArBill selectArBillById(Long arBillId);
|
||||
|
||||
ArBill selectArBillByIdScoped(@Param("arBillId") Long arBillId, @Param("scope") ArBillRowScope scope);
|
||||
|
||||
ArBill selectArBillByBillNo(String billNo);
|
||||
|
||||
ArBill selectGeneratingByCustomerPeriod(@Param("customerId") Long customerId,
|
||||
@@ -29,7 +32,7 @@ public interface ArBillMapper {
|
||||
/**
|
||||
* 查询客户可核销账单(已确认/部分核销),按账单日期升序,供余额核销选择
|
||||
*/
|
||||
List<ArBill> selectUnsettledByCustomer(@Param("customerId") Long customerId);
|
||||
List<ArBill> selectUnsettledByCustomer(@Param("customerId") Long customerId, @Param("scope") ArBillRowScope scope);
|
||||
|
||||
int insertArBill(ArBill bill);
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.bms.ar.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.ar.security.ArBillRowScope;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
|
||||
|
||||
public interface ArBillScopeMapper {
|
||||
int countCustomerSourceBills(@Param("customerId") Long customerId, @Param("sourceIds") List<String> sourceIds);
|
||||
|
||||
int countAccessibleBill(@Param("arBillId") Long arBillId, @Param("scope") ArBillRowScope scope);
|
||||
|
||||
BmsCustomerOption selectAccessibleCustomer(@Param("customerId") Long customerId, @Param("scope") ArBillRowScope scope);
|
||||
|
||||
List<BmsCustomerOption> selectCustomerOptions(@Param("keywords") List<String> keywords,
|
||||
@Param("scope") ArBillRowScope scope);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.bms.ar.security;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/** 应收业务范围,与角色管理的 dataScope 配置无关。 */
|
||||
public final class ArBillRoleKeys {
|
||||
public static final String SALES = "ORA_6041";
|
||||
public static final String BUSINESS_SPECIALIST = "ORA_6163";
|
||||
public static final String SALES_MANAGER = "ORA_6040";
|
||||
/** 销售员、商务专员:本人负责客户的账单。 */
|
||||
public static final Set<String> SELF_DATA_ROLES = Set.of(SALES, BUSINESS_SPECIALIST);
|
||||
/** 财务经理、应收财务;仍须具备对应接口的功能权限。 */
|
||||
public static final Set<String> ALL_DATA_ROLES = Set.of("ORA_6042", "ORA_6043");
|
||||
|
||||
static {
|
||||
if (SELF_DATA_ROLES.contains(SALES_MANAGER) || ALL_DATA_ROLES.contains(SALES_MANAGER)
|
||||
|| SELF_DATA_ROLES.stream().anyMatch(ALL_DATA_ROLES::contains)) {
|
||||
throw new IllegalStateException("应收数据权限角色映射冲突");
|
||||
}
|
||||
}
|
||||
|
||||
private ArBillRoleKeys() {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.bms.ar.security;
|
||||
|
||||
/** 仅由后端构造,不能绑定到 HTTP 查询对象。 */
|
||||
public final class ArBillRowScope {
|
||||
private final boolean all;
|
||||
private final boolean self;
|
||||
private final boolean department;
|
||||
private final Long userId;
|
||||
private final Long deptId;
|
||||
|
||||
ArBillRowScope(boolean all, boolean self, boolean department, Long userId, Long deptId) {
|
||||
this.all = all;
|
||||
this.self = self && userId != null && userId > 0;
|
||||
this.department = department && deptId != null && deptId > 0;
|
||||
this.userId = userId;
|
||||
this.deptId = deptId;
|
||||
}
|
||||
|
||||
public boolean isAll() { return all; }
|
||||
public boolean isSelf() { return self; }
|
||||
public boolean isDepartment() { return department; }
|
||||
public Long getUserId() { return userId; }
|
||||
public Long getDeptId() { return deptId; }
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.bms.ar.security;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import com.github.pagehelper.Page;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.bms.ar.mapper.ArBillScopeMapper;
|
||||
import com.bms.common.constant.Constants;
|
||||
import com.bms.common.core.domain.entity.SysRole;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.core.domain.model.LoginUser;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.PermissionContext;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
|
||||
|
||||
@Component
|
||||
public class ArBillScopeResolver {
|
||||
private final ArBillScopeMapper mapper;
|
||||
|
||||
public ArBillScopeResolver(ArBillScopeMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
/** 只读取登录态;功能上下文由 @ss.hasPermi 写入,缺失时不授权。 */
|
||||
public ArBillRowScope resolve() {
|
||||
return resolve(SecurityUtils.getLoginUser(), PermissionContext.get());
|
||||
}
|
||||
|
||||
public ArBillRowScope resolve(LoginUser actor, String permission) {
|
||||
if (actor == null || actor.getUser() == null || actor.getUserId() == null) {
|
||||
throw new ServiceException("登录状态无效,请重新登录", 401);
|
||||
}
|
||||
SysUser user = actor.getUser();
|
||||
if (!"0".equals(user.getStatus()) || !"0".equals(user.getDelFlag())) {
|
||||
return new ArBillRowScope(false, false, false, actor.getUserId(), actor.getDeptId());
|
||||
}
|
||||
if (user.isAdmin()) {
|
||||
return new ArBillRowScope(true, false, false, actor.getUserId(), actor.getDeptId());
|
||||
}
|
||||
boolean all = false;
|
||||
boolean self = false;
|
||||
boolean department = false;
|
||||
if (permission != null && !permission.isBlank() && user.getRoles() != null) {
|
||||
for (SysRole role : user.getRoles()) {
|
||||
if (role == null || !"0".equals(role.getStatus()) || !permits(role.getPermissions(), permission)) continue;
|
||||
String key = role.getRoleKey();
|
||||
all |= ArBillRoleKeys.ALL_DATA_ROLES.contains(key == null ? "" : key);
|
||||
self |= ArBillRoleKeys.SELF_DATA_ROLES.contains(key == null ? "" : key);
|
||||
department |= ArBillRoleKeys.SALES_MANAGER.equals(key);
|
||||
}
|
||||
}
|
||||
return new ArBillRowScope(all, self, department, actor.getUserId(), actor.getDeptId());
|
||||
}
|
||||
|
||||
private boolean permits(Set<String> permissions, String required) {
|
||||
return permissions != null && (permissions.contains(Constants.ALL_PERMISSION)
|
||||
|| Arrays.stream(required.split(Constants.PERMISSION_DELIMITER)).map(String::trim).anyMatch(permissions::contains));
|
||||
}
|
||||
|
||||
public void requireBillAccessible(Long arBillId) {
|
||||
ArBillRowScope scope = resolve();
|
||||
if (arBillId == null || withoutPagination(() -> mapper.countAccessibleBill(arBillId, scope)) == 0) {
|
||||
throw new ServiceException("账单不存在或无权访问");
|
||||
}
|
||||
}
|
||||
|
||||
public BmsCustomerOption requireCustomerAccessible(Long customerId) {
|
||||
ArBillRowScope scope = resolve();
|
||||
BmsCustomerOption customer = customerId == null ? null
|
||||
: withoutPagination(() -> mapper.selectAccessibleCustomer(customerId, scope));
|
||||
if (customer == null) throw new ServiceException("客户不存在或无权访问");
|
||||
return customer;
|
||||
}
|
||||
|
||||
public List<BmsCustomerOption> selectCustomerOptions(String keywords) {
|
||||
List<String> values = keywords == null ? List.of()
|
||||
: Arrays.stream(keywords.split("\\R")).map(String::trim).filter(s -> !s.isEmpty()).distinct().toList();
|
||||
return mapper.selectCustomerOptions(values, resolve());
|
||||
}
|
||||
|
||||
public void requireCustomerSourceBills(Long customerId, List<String> sourceIds) {
|
||||
if (!sourceIds.isEmpty() && withoutPagination(() -> mapper.countCustomerSourceBills(customerId, sourceIds)) != sourceIds.size()) {
|
||||
throw new ServiceException("所选订单不存在或不属于当前客户");
|
||||
}
|
||||
}
|
||||
|
||||
/** 资源预校验不能消耗 Controller 已设置的分页,最终业务查询仍使用原分页对象。 */
|
||||
private <T> T withoutPagination(Supplier<T> query) {
|
||||
Page<?> page = PageHelper.getLocalPage();
|
||||
PageHelper.clearPage();
|
||||
try {
|
||||
return query.get();
|
||||
} finally {
|
||||
if (page != null) PageHelper.setLocalPage(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -23,6 +23,7 @@ import com.bms.ar.enums.ArLogOperationType;
|
||||
import com.bms.ar.enums.ArLogResult;
|
||||
import com.bms.ar.mapper.ArBalanceWriteoffMapper;
|
||||
import com.bms.ar.mapper.ArBillMapper;
|
||||
import com.bms.ar.security.ArBillScopeResolver;
|
||||
import com.bms.ar.service.IArBalanceWriteoffService;
|
||||
import com.bms.ar.service.IArBillLogService;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
@@ -46,6 +47,8 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
|
||||
@Autowired
|
||||
private ArBillMapper billMapper;
|
||||
@Autowired
|
||||
private ArBillScopeResolver billScope;
|
||||
|
||||
@Autowired
|
||||
private ArBalanceWriteoffMapper writeoffMapper;
|
||||
@@ -67,7 +70,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
if (customerId == null) {
|
||||
return List.of();
|
||||
}
|
||||
return billMapper.selectUnsettledByCustomer(customerId);
|
||||
return billMapper.selectUnsettledByCustomer(customerId, billScope.resolve());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +78,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
*/
|
||||
@Override
|
||||
public List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query) {
|
||||
return writeoffMapper.selectWriteoffList(query);
|
||||
return writeoffMapper.selectWriteoffList(query, billScope.resolve());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,6 +106,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
if (bill == null) {
|
||||
throw new ServiceException("应收账单不存在");
|
||||
}
|
||||
billScope.requireBillAccessible(bill.getArBillId());
|
||||
ArBillStatus status = ArBillStatus.of(bill.getStatus());
|
||||
if (status == null || !status.canWriteOff()) {
|
||||
throw new ServiceException("当前账单状态不允许核销:" + (status == null ? bill.getStatus() : status.getLabel()));
|
||||
@@ -170,6 +174,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
if (writeoff == null) {
|
||||
throw new ServiceException("核销记录不存在");
|
||||
}
|
||||
billScope.requireBillAccessible(writeoff.getArBillId());
|
||||
if (!ArBalanceWriteoffStatus.EFFECTIVE.getCode().equals(writeoff.getStatus())) {
|
||||
throw new ServiceException("只有有效状态的核销记录可以反核销");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.bms.ar.enums.ArLogOperationSource;
|
||||
import com.bms.ar.enums.ArLogOperationType;
|
||||
import com.bms.ar.enums.ArLogResult;
|
||||
import com.bms.ar.mapper.ArBillLogMapper;
|
||||
import com.bms.ar.security.ArBillScopeResolver;
|
||||
import com.bms.ar.service.IArBillLogService;
|
||||
import com.bms.common.utils.uuid.IdUtils;
|
||||
|
||||
@@ -19,10 +20,12 @@ import com.bms.common.utils.uuid.IdUtils;
|
||||
public class ArBillLogServiceImpl implements IArBillLogService {
|
||||
@Autowired
|
||||
private ArBillLogMapper logMapper;
|
||||
@Autowired
|
||||
private ArBillScopeResolver billScope;
|
||||
|
||||
@Override
|
||||
public List<ArBillLog> selectLogList(ArBillLog query) {
|
||||
return logMapper.selectList(query);
|
||||
return logMapper.selectList(query, billScope.resolve());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -70,6 +70,8 @@ import com.bms.ar.mapper.ArBillDetailMapper;
|
||||
import com.bms.ar.mapper.ArBillExportMapper;
|
||||
import com.bms.ar.mapper.ArBillLogMapper;
|
||||
import com.bms.ar.mapper.ArBillMapper;
|
||||
import com.bms.ar.security.ArBillScopeResolver;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
|
||||
import com.bms.ar.mapper.ArBillGenerateTaskMapper;
|
||||
import com.bms.ar.mapper.ArBankAccountMapper;
|
||||
import com.bms.ar.mapper.ArExportTemplateMapper;
|
||||
@@ -182,6 +184,8 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
@Autowired
|
||||
private ArBillMapper billMapper;
|
||||
@Autowired
|
||||
private ArBillScopeResolver billScope;
|
||||
@Autowired
|
||||
private ArBillGenerateTaskMapper generateTaskMapper;
|
||||
@Autowired
|
||||
private ArBankAccountMapper bankAccountMapper;
|
||||
@@ -214,7 +218,8 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
@Override
|
||||
public List<ArBill> selectArBillList(ArBillQuery query) {
|
||||
long startedAt = System.nanoTime();
|
||||
List<ArBill> bills = billMapper.selectArBillList(query);
|
||||
query = query == null ? new ArBillQuery() : query;
|
||||
List<ArBill> bills = billMapper.selectArBillList(query, billScope.resolve());
|
||||
for (ArBill bill : bills) {
|
||||
bill.setBankAccountNo(maskAccount(bill.getBankAccountNo()));
|
||||
}
|
||||
@@ -229,7 +234,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
*/
|
||||
@Override
|
||||
public ArBillSummary selectArBillSummary(ArBillQuery query) {
|
||||
return billMapper.selectArBillSummary(query);
|
||||
return billMapper.selectArBillSummary(query == null ? new ArBillQuery() : query, billScope.resolve());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,9 +243,9 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
@Override
|
||||
public ArBill selectArBillById(Long arBillId) {
|
||||
long startedAt = System.nanoTime();
|
||||
ArBill bill = billMapper.selectArBillById(arBillId);
|
||||
ArBill bill = billMapper.selectArBillByIdScoped(arBillId, billScope.resolve());
|
||||
if (bill == null) {
|
||||
throw new ServiceException("应收账单不存在");
|
||||
throw new ServiceException("账单不存在或无权访问");
|
||||
}
|
||||
bill.setBankAccountNo(maskAccount(bill.getBankAccountNo()));
|
||||
bill.setExports(exportMapper.selectByBillId(arBillId));
|
||||
@@ -256,7 +261,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
@Override
|
||||
public List<ArBillDetail> selectArBillDetails(Long arBillId) {
|
||||
long startedAt = System.nanoTime();
|
||||
List<ArBillDetail> details = detailMapper.selectByBillId(arBillId);
|
||||
List<ArBillDetail> details = detailMapper.selectByBillIdScoped(arBillId, billScope.resolve());
|
||||
log.debug("应收账单明细查询完成,arBillId={}, resultCount={}, costMs={}",
|
||||
arBillId, details.size(), elapsedMillis(startedAt));
|
||||
return details;
|
||||
@@ -268,7 +273,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
@Override
|
||||
public List<ArBillDetailFee> selectArBillDetailFees(Long arBillId, Long detailId) {
|
||||
long startedAt = System.nanoTime();
|
||||
List<ArBillDetailFee> fees = detailFeeMapper.selectByBillIdAndDetailId(arBillId, detailId);
|
||||
List<ArBillDetailFee> fees = detailFeeMapper.selectByBillIdAndDetailIdScoped(arBillId, detailId, billScope.resolve());
|
||||
log.debug("应收账单费用明细查询完成,arBillId={}, detailId={}, resultCount={}, costMs={}",
|
||||
arBillId, detailId, fees.size(), elapsedMillis(startedAt));
|
||||
return fees;
|
||||
@@ -308,6 +313,8 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
public List<PrcOrderBill> previewBill(ArBillGenerateQuery query) {
|
||||
long startedAt = System.nanoTime();
|
||||
validatePreviewQuery(query);
|
||||
BmsCustomerOption customer = billScope.requireCustomerAccessible(query.getCustomerId());
|
||||
query.setCustomerCode(customer.getCustomerCode());
|
||||
List<String> orderIdentifiers = normalizeOrderIdentifiers(query.getOrderOrTrackingNo());
|
||||
List<String> excludeOrderIdentifiers = normalizeOrderIdentifiers(query.getExcludeOrderOrTrackingNo());
|
||||
List<PrcOrderBill> result = sourceBillMapper.selectPrcOrderBillByCustomerPeriod(query.getCustomerId(),
|
||||
@@ -336,6 +343,8 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
public BigDecimal sumPreviewBill(ArBillGenerateQuery query) {
|
||||
long startedAt = System.nanoTime();
|
||||
validatePreviewQuery(query);
|
||||
BmsCustomerOption customer = billScope.requireCustomerAccessible(query.getCustomerId());
|
||||
query.setCustomerCode(customer.getCustomerCode());
|
||||
List<String> orderIdentifiers = normalizeOrderIdentifiers(query.getOrderOrTrackingNo());
|
||||
List<String> excludeOrderIdentifiers = normalizeOrderIdentifiers(query.getExcludeOrderOrTrackingNo());
|
||||
BigDecimal total = nvl(sourceBillMapper.sumPrcOrderBillIncomeByCustomerPeriod(query.getCustomerId(),
|
||||
@@ -359,8 +368,12 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
long startedAt = System.nanoTime();
|
||||
Long createUserId = SecurityUtils.getUserId();
|
||||
validatePeriod(request);
|
||||
BmsCustomerOption customer = billScope.requireCustomerAccessible(request.getCustomerId());
|
||||
request.setCustomerCode(customer.getCustomerCode());
|
||||
request.setCustomerName(customer.getCustomerName());
|
||||
ArBillGenerateTask existed = generateTaskMapper.selectByRequestToken(requestId);
|
||||
if (existed != null) {
|
||||
billScope.requireBillAccessible(existed.getArBillId());
|
||||
log.info("应收账单生成请求命中幂等记录,operator={}, requestId={}, taskId={}, status={}, costMs={}",
|
||||
safeLogValue(operator), safeLogValue(requestId), existed.getTaskId(), existed.getStatus(),
|
||||
elapsedMillis(startedAt));
|
||||
@@ -401,6 +414,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
validateOrderIdentifierParameterCount(
|
||||
orderIdentifierCount,
|
||||
allMatched ? excludedSourceBillIds.size() : sourceBillIds.size());
|
||||
billScope.requireCustomerSourceBills(request.getCustomerId(), allMatched ? excludedSourceBillIds : sourceBillIds);
|
||||
request.setSelectionMode(selectionMode);
|
||||
|
||||
String currency = StringUtils.isEmpty(request.getCurrency()) ? "CNY" : request.getCurrency().trim();
|
||||
@@ -478,8 +492,9 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
long startedAt = System.nanoTime();
|
||||
ArBillGenerateTask task = generateTaskMapper.selectById(taskId);
|
||||
if (task == null) {
|
||||
throw new ServiceException("账单生成任务不存在");
|
||||
throw new ServiceException("账单不存在或无权访问");
|
||||
}
|
||||
billScope.requireBillAccessible(task.getArBillId());
|
||||
log.debug("应收账单生成任务状态查询,taskId={}, status={}, processedCount={}, totalCount={}, costMs={}",
|
||||
taskId, task.getStatus(), task.getProcessedCount(), task.getTotalCount(), elapsedMillis(startedAt));
|
||||
return task;
|
||||
@@ -491,7 +506,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
@Override
|
||||
public List<ArBillGenerateTask> selectActiveGenerateTasks(String operator) {
|
||||
long startedAt = System.nanoTime();
|
||||
List<ArBillGenerateTask> tasks = generateTaskMapper.selectActiveByCreateBy(operator);
|
||||
List<ArBillGenerateTask> tasks = generateTaskMapper.selectActiveByCreateByScoped(operator, billScope.resolve());
|
||||
log.debug("当前用户应收账单生成任务查询,operator={}, taskCount={}, costMs={}",
|
||||
safeLogValue(operator), tasks.size(), elapsedMillis(startedAt));
|
||||
return tasks;
|
||||
@@ -504,7 +519,9 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ArBillGenerateTask cancelGenerateTask(Long taskId, String operator, String requestId) {
|
||||
long startedAt = System.nanoTime();
|
||||
ArBillGenerateTask task = selectGenerateTask(taskId);
|
||||
ArBillGenerateTask task = generateTaskMapper.selectById(taskId);
|
||||
if (task == null) throw new ServiceException("账单不存在或无权访问");
|
||||
billScope.requireBillAccessible(task.getArBillId());
|
||||
if (!operator.equals(task.getCreateBy())) {
|
||||
throw new ServiceException("只能取消自己创建的账单生成任务");
|
||||
}
|
||||
@@ -533,7 +550,9 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ArBillGenerateTask retryGenerateTask(Long taskId, String operator, String requestId) {
|
||||
long startedAt = System.nanoTime();
|
||||
ArBillGenerateTask task = selectGenerateTask(taskId);
|
||||
ArBillGenerateTask task = generateTaskMapper.selectById(taskId);
|
||||
if (task == null) throw new ServiceException("账单不存在或无权访问");
|
||||
billScope.requireBillAccessible(task.getArBillId());
|
||||
if (!operator.equals(task.getCreateBy())) {
|
||||
throw new ServiceException("只能重新生成自己创建的账单任务");
|
||||
}
|
||||
@@ -923,6 +942,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
public ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId) {
|
||||
long startedAt = System.nanoTime();
|
||||
ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId);
|
||||
billScope.requireBillAccessible(arBillId);
|
||||
if (bill == null) {
|
||||
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.CONFIRM,
|
||||
null, "应收账单不存在", operator, requestId);
|
||||
@@ -972,6 +992,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
null, "作废必须填写原因", operator, requestId);
|
||||
}
|
||||
ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId);
|
||||
billScope.requireBillAccessible(arBillId);
|
||||
if (bill == null) {
|
||||
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID,
|
||||
null, "应收账单不存在", operator, requestId);
|
||||
@@ -1011,11 +1032,13 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
public ArBillExport exportBill(Long arBillId, String operator, String requestId) {
|
||||
long startedAt = System.nanoTime();
|
||||
Long exportUserId = SecurityUtils.getUserId();
|
||||
billScope.requireBillAccessible(arBillId);
|
||||
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, ArLogObjectType.EXPORT.getCode(),
|
||||
ArLogResult.SUCCESS.getCode());
|
||||
if (previous != null && isNumber(previous.getObjectId())) {
|
||||
ArBillExport previousExport = exportMapper.selectById(Long.valueOf(previous.getObjectId()));
|
||||
if (previousExport != null) {
|
||||
billScope.requireBillAccessible(previousExport.getArBillId());
|
||||
log.info("应收账单导出请求命中幂等记录,operator={}, requestId={}, arBillId={}, exportId={}, status={}, costMs={}",
|
||||
safeLogValue(operator), safeLogValue(requestId), arBillId, previousExport.getExportId(),
|
||||
previousExport.getExportStatus(), elapsedMillis(startedAt));
|
||||
@@ -1084,9 +1107,8 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
public ArBillExport selectExport(Long exportId) {
|
||||
long startedAt = System.nanoTime();
|
||||
ArBillExport export = exportMapper.selectById(exportId);
|
||||
if (export == null) {
|
||||
throw new ServiceException("导出任务不存在");
|
||||
}
|
||||
if (export == null) throw new ServiceException("账单不存在或无权访问");
|
||||
billScope.requireBillAccessible(export.getArBillId());
|
||||
log.debug("应收账单导出任务状态查询,exportId={}, status={}, costMs={}",
|
||||
exportId, export.getExportStatus(), elapsedMillis(startedAt));
|
||||
return export;
|
||||
@@ -1167,6 +1189,7 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
@Override
|
||||
public List<ArBillExport> selectExports(Long arBillId) {
|
||||
long startedAt = System.nanoTime();
|
||||
billScope.requireBillAccessible(arBillId);
|
||||
List<ArBillExport> exports = exportMapper.selectByBillId(arBillId);
|
||||
log.debug("应收账单导出记录查询,arBillId={}, exportCount={}, costMs={}",
|
||||
arBillId, exports.size(), elapsedMillis(startedAt));
|
||||
@@ -1177,7 +1200,11 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
public void downloadExport(Long exportId, HttpServletResponse response) throws Exception {
|
||||
long startedAt = System.nanoTime();
|
||||
ArBillExport export = exportMapper.selectById(exportId);
|
||||
if (export == null || !ArExportStatus.SUCCESS.getCode().equals(export.getExportStatus()) || StringUtils.isEmpty(export.getStorageKey())) {
|
||||
if (export == null) {
|
||||
throw new ServiceException("账单不存在或无权访问");
|
||||
}
|
||||
billScope.requireBillAccessible(export.getArBillId());
|
||||
if (!ArExportStatus.SUCCESS.getCode().equals(export.getExportStatus()) || StringUtils.isEmpty(export.getStorageKey())) {
|
||||
throw new ServiceException("导出文件不存在或尚未生成成功");
|
||||
}
|
||||
String resource = export.getStorageKey();
|
||||
|
||||
@@ -77,22 +77,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where writeoff_id = #{writeoffId} and status = #{effectiveStatus}
|
||||
</update>
|
||||
|
||||
<select id="selectWriteoffList" parameterType="ArBalanceWriteoff" resultMap="ArBalanceWriteoffResult">
|
||||
<select id="selectWriteoffList" resultMap="ArBalanceWriteoffResult">
|
||||
<include refid="selectColumns"/>
|
||||
<where>
|
||||
<if test="customerId != null">and w.customer_id = #{customerId}</if>
|
||||
<if test="arBillId != null">and w.ar_bill_id = #{arBillId}</if>
|
||||
<if test="status != null and status != ''">and w.status = #{status}</if>
|
||||
<if test="currency != null and currency != ''">and w.currency = #{currency}</if>
|
||||
<if test="writeoffNo != null and writeoffNo != ''">and w.writeoff_no like concat('%', #{writeoffNo}, '%')</if>
|
||||
<if test="billNo != null and billNo != ''">and w.bill_no like concat('%', #{billNo}, '%')</if>
|
||||
<if test="receiptId != null">and w.receipt_id = #{receiptId}</if>
|
||||
<if test="params != null and params.beginTime != null and params.beginTime != ''">
|
||||
and date_format(w.writeoff_date,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d')
|
||||
<if test="query.customerId != null">and w.customer_id = #{query.customerId}</if>
|
||||
<if test="query.arBillId != null">and w.ar_bill_id = #{query.arBillId}</if>
|
||||
<if test="query.status != null and query.status != ''">and w.status = #{query.status}</if>
|
||||
<if test="query.currency != null and query.currency != ''">and w.currency = #{query.currency}</if>
|
||||
<if test="query.writeoffNo != null and query.writeoffNo != ''">and w.writeoff_no like concat('%', #{query.writeoffNo}, '%')</if>
|
||||
<if test="query.billNo != null and query.billNo != ''">and w.bill_no like concat('%', #{query.billNo}, '%')</if>
|
||||
<if test="query.receiptId != null">and w.receipt_id = #{query.receiptId}</if>
|
||||
<if test="query.params != null and query.params.beginTime != null and query.params.beginTime != ''">
|
||||
and date_format(w.writeoff_date,'%y%m%d') >= date_format(#{query.params.beginTime},'%y%m%d')
|
||||
</if>
|
||||
<if test="params != null and params.endTime != null and params.endTime != ''">
|
||||
and date_format(w.writeoff_date,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
|
||||
<if test="query.params != null and query.params.endTime != null and query.params.endTime != ''">
|
||||
and date_format(w.writeoff_date,'%y%m%d') <= date_format(#{query.params.endTime},'%y%m%d')
|
||||
</if>
|
||||
<include refid="com.bms.ar.mapper.ArBillScopeMapper.billScope"/>
|
||||
</where>
|
||||
order by w.create_time desc, w.writeoff_id desc
|
||||
</select>
|
||||
|
||||
@@ -78,6 +78,34 @@
|
||||
order by f.fee_no, f.detail_fee_id
|
||||
</select>
|
||||
|
||||
<select id="selectByBillIdAndDetailIdScoped" resultMap="ArBillDetailFeeResult">
|
||||
select f.detail_fee_id,
|
||||
f.detail_id,
|
||||
f.prc_income_bill_fee_id,
|
||||
f.fee_no,
|
||||
f.fee_type,
|
||||
f.fee_name,
|
||||
f.sc_id,
|
||||
f.quantity,
|
||||
f.unit,
|
||||
f.unit_price,
|
||||
f.amount,
|
||||
f.product_amount,
|
||||
f.redline_amount,
|
||||
f.currency,
|
||||
f.calculation_formula,
|
||||
f.round_mode,
|
||||
f.remark,
|
||||
f.create_by,
|
||||
f.create_time
|
||||
from ar_bill_detail_fee f
|
||||
inner join ar_bill_detail d on d.detail_id = f.detail_id
|
||||
where d.ar_bill_id = #{arBillId}
|
||||
and f.detail_id = #{detailId}
|
||||
AND EXISTS (SELECT 1 FROM ar_bill b WHERE b.ar_bill_id = d.ar_bill_id <include refid="com.bms.ar.mapper.ArBillScopeMapper.billScope"/>)
|
||||
order by f.fee_no, f.detail_fee_id
|
||||
</select>
|
||||
|
||||
<select id="selectByBillId" parameterType="long" resultMap="ArBillDetailFeeResult">
|
||||
select detail_fee_id,
|
||||
detail_id,
|
||||
|
||||
@@ -46,6 +46,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from ar_bill_detail where ar_bill_id = #{arBillId} order by detail_no
|
||||
</select>
|
||||
|
||||
<select id="selectByBillIdScoped" resultMap="ArBillDetailResult">
|
||||
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
|
||||
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
|
||||
actual_weight, receive_weight, volume_weight, charge_weight, freight_rate, registration_fee, pkg_length, pkg_width, pkg_height,
|
||||
currency, total_amount, product_amount, redline_amount, billing_fail_status, remark, create_by, create_time
|
||||
from ar_bill_detail where ar_bill_id = #{arBillId} AND EXISTS (SELECT 1 FROM ar_bill b WHERE b.ar_bill_id = ar_bill_detail.ar_bill_id <include refid="com.bms.ar.mapper.ArBillScopeMapper.billScope"/>)
|
||||
order by detail_no
|
||||
</select>
|
||||
|
||||
<select id="selectByBillIdAndSourceIds" resultMap="ArBillDetailResult">
|
||||
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
|
||||
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
|
||||
|
||||
@@ -69,6 +69,15 @@
|
||||
order by create_time desc, task_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectActiveByCreateByScoped" resultMap="ArBillGenerateTaskResult">
|
||||
<include refid="columns"/>
|
||||
where create_by = #{createBy}
|
||||
and status in ('WAITING', 'RUNNING', 'CANCEL_REQUESTED')
|
||||
AND EXISTS (SELECT 1 FROM ar_bill b WHERE b.ar_bill_id = ar_bill_generate_task.ar_bill_id
|
||||
<include refid="com.bms.ar.mapper.ArBillScopeMapper.billScope"/>)
|
||||
order by create_time desc, task_id desc
|
||||
</select>
|
||||
|
||||
<!-- 定时调度器按创建时间顺序取待处理任务。 -->
|
||||
<select id="selectWaiting" resultMap="ArBillGenerateTaskResult">
|
||||
<include refid="columns"/>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<result property="createTime" column="create_time"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectList" parameterType="ArBillLog" resultMap="ArBillLogResult">
|
||||
<select id="selectList" resultMap="ArBillLogResult">
|
||||
select l.log_id, l.ar_bill_id, l.object_type, l.object_id, l.operation_type, l.operation_desc,
|
||||
case l.object_type
|
||||
when 'BILL' then b.bill_no
|
||||
@@ -43,14 +43,15 @@
|
||||
left join ar_balance_writeoff bw on bw.writeoff_id = case when l.object_type = 'BALANCE_WRITEOFF' then
|
||||
cast(l.object_id as unsigned) end
|
||||
<where>
|
||||
<if test="arBillId != null">and l.ar_bill_id = #{arBillId}</if>
|
||||
<if test="objectType != null and objectType != ''">and l.object_type = #{objectType}</if>
|
||||
<if test="objectId != null and objectId != ''">and l.object_id = #{objectId}</if>
|
||||
<if test="operationType != null and operationType != ''">and l.operation_type = #{operationType}</if>
|
||||
<if test="operationSource != null and operationSource != ''">and l.operation_source = #{operationSource}
|
||||
<if test="query.arBillId != null">and l.ar_bill_id = #{query.arBillId}</if>
|
||||
<if test="query.objectType != null and query.objectType != ''">and l.object_type = #{query.objectType}</if>
|
||||
<if test="query.objectId != null and query.objectId != ''">and l.object_id = #{query.objectId}</if>
|
||||
<if test="query.operationType != null and query.operationType != ''">and l.operation_type = #{query.operationType}</if>
|
||||
<if test="query.operationSource != null and query.operationSource != ''">and l.operation_source = #{query.operationSource}
|
||||
</if>
|
||||
<if test="requestId != null and requestId != ''">and l.request_id = #{requestId}</if>
|
||||
<if test="result != null and result != ''">and l.result = #{result}</if>
|
||||
<if test="query.requestId != null and query.requestId != ''">and l.request_id = #{query.requestId}</if>
|
||||
<if test="query.result != null and query.result != ''">and l.result = #{query.result}</if>
|
||||
<include refid="com.bms.ar.mapper.ArBillScopeMapper.billScope"/>
|
||||
</where>
|
||||
order by l.create_time desc, l.log_id desc
|
||||
</select>
|
||||
|
||||
@@ -76,34 +76,35 @@
|
||||
<sql id="arBillListFilter">
|
||||
<where>
|
||||
and (generation_status is null or generation_status = 'READY')
|
||||
<if test="billNo != null and billNo != ''">and bill_no like concat('%', #{billNo}, '%')</if>
|
||||
<if test="billName != null and billName != ''">and bill_name like concat('%', #{billName}, '%')</if>
|
||||
<if test="customerCode != null and customerCode != ''">and customer_code like concat('%', #{customerCode},
|
||||
<if test="query.billNo != null and query.billNo != ''">and bill_no like concat('%', #{query.billNo}, '%')</if>
|
||||
<if test="query.billName != null and query.billName != ''">and bill_name like concat('%', #{query.billName}, '%')</if>
|
||||
<if test="query.customerCode != null and query.customerCode != ''">and customer_code like concat('%', #{query.customerCode},
|
||||
'%')
|
||||
</if>
|
||||
<if test="customerName != null and customerName != ''">and customer_name like concat('%', #{customerName},
|
||||
<if test="query.customerName != null and query.customerName != ''">and customer_name like concat('%', #{query.customerName},
|
||||
'%')
|
||||
</if>
|
||||
<if test="customerId != null">and customer_id = #{customerId}</if>
|
||||
<if test="status != null and status != ''">and status = #{status}</if>
|
||||
<if test="currency != null and currency != ''">and currency = #{currency}</if>
|
||||
<if test="billDateStart != null">and bill_date >= #{billDateStart}</if>
|
||||
<if test="billDateEnd != null">and bill_date <= #{billDateEnd}</if>
|
||||
<if test="billPeriodEnd != null">and bill_period_end = #{billPeriodEnd}</if>
|
||||
<if test="query.customerId != null">and customer_id = #{query.customerId}</if>
|
||||
<if test="query.status != null and query.status != ''">and status = #{query.status}</if>
|
||||
<if test="query.currency != null and query.currency != ''">and currency = #{query.currency}</if>
|
||||
<if test="query.billDateStart != null">and bill_date >= #{query.billDateStart}</if>
|
||||
<if test="query.billDateEnd != null">and bill_date <= #{query.billDateEnd}</if>
|
||||
<if test="query.billPeriodEnd != null">and bill_period_end = #{query.billPeriodEnd}</if>
|
||||
<include refid="com.bms.ar.mapper.ArBillScopeMapper.billScope"/>
|
||||
</where>
|
||||
</sql>
|
||||
|
||||
<select id="selectArBillList" parameterType="com.bms.ar.domain.vo.ArBillQuery" resultMap="ArBillResult">
|
||||
<include refid="selectArBillColumns"/>
|
||||
<select id="selectArBillList" resultMap="ArBillResult">
|
||||
select b.* from ar_bill b
|
||||
<include refid="arBillListFilter"/>
|
||||
order by create_time desc, ar_bill_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectArBillSummary" parameterType="com.bms.ar.domain.vo.ArBillQuery" resultMap="ArBillSummaryResult">
|
||||
<select id="selectArBillSummary" resultMap="ArBillSummaryResult">
|
||||
select coalesce(sum(total_amount), 0) as total_amount,
|
||||
coalesce(sum(written_off_amount), 0) as written_off_amount,
|
||||
coalesce(sum(unwritten_amount), 0) as unwritten_amount
|
||||
from ar_bill
|
||||
from ar_bill b
|
||||
<include refid="arBillListFilter"/>
|
||||
</select>
|
||||
|
||||
@@ -112,6 +113,11 @@
|
||||
where ar_bill_id = #{arBillId}
|
||||
</select>
|
||||
|
||||
<select id="selectArBillByIdScoped" resultMap="ArBillResult">
|
||||
select b.* from ar_bill b where b.ar_bill_id = #{arBillId}
|
||||
<include refid="com.bms.ar.mapper.ArBillScopeMapper.billScope"/>
|
||||
</select>
|
||||
|
||||
<select id="selectArBillByBillNo" parameterType="string" resultMap="ArBillResult">
|
||||
<include refid="selectArBillColumns"/>
|
||||
where bill_no = #{billNo}
|
||||
@@ -144,11 +150,12 @@
|
||||
</select>
|
||||
|
||||
<select id="selectUnsettledByCustomer" resultMap="ArBillResult">
|
||||
<include refid="selectArBillColumns"/>
|
||||
select b.* from ar_bill b
|
||||
where customer_id = #{customerId}
|
||||
and status in ('CONFIRMED', 'PARTIAL_WRITTEN_OFF')
|
||||
and (generation_status is null or generation_status = 'READY')
|
||||
and unwritten_amount > 0
|
||||
<include refid="com.bms.ar.mapper.ArBillScopeMapper.billScope"/>
|
||||
order by bill_date asc, ar_bill_id asc
|
||||
</select>
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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.bms.ar.mapper.ArBillScopeMapper">
|
||||
<resultMap id="CustomerOption" type="com.bms.customer.customerinfo.domain.BmsCustomerOption">
|
||||
<id property="customerId" column="customer_id"/>
|
||||
<result property="customerCode" column="customer_code"/>
|
||||
<result property="customerName" column="customer_name"/>
|
||||
</resultMap>
|
||||
<select id="countCustomerSourceBills" resultType="int">
|
||||
SELECT COUNT(*) FROM prc_order_bill WHERE customer_id = #{customerId}
|
||||
AND bill_id IN
|
||||
<foreach collection="sourceIds" item="sourceId" open="(" separator="," close=")">#{sourceId}</foreach>
|
||||
</select>
|
||||
<!-- c 固定为客户表别名;无部门层级扩展,不按角色管理 data_scope 过滤。 -->
|
||||
<sql id="customerScope">
|
||||
<choose>
|
||||
<when test="scope != null and scope.all"/>
|
||||
<when test="scope != null and (scope.self or scope.department)">
|
||||
AND c.del_flag = '0'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM sys_user su
|
||||
WHERE CAST(su.user_id AS CHAR) = c.sales_user_id AND su.del_flag = '0'
|
||||
AND (1 = 0
|
||||
<if test="scope.self">OR su.user_id = #{scope.userId}</if>
|
||||
<if test="scope.department">OR su.dept_id = #{scope.deptId}</if>
|
||||
)
|
||||
)
|
||||
</when>
|
||||
<otherwise>AND 1 = 0</otherwise>
|
||||
</choose>
|
||||
</sql>
|
||||
<!-- b 固定为账单表别名;ALL 无需关联客户,保留归属异常的历史账单。 -->
|
||||
<sql id="billScope">
|
||||
<choose>
|
||||
<when test="scope != null and scope.all"/>
|
||||
<otherwise>
|
||||
AND EXISTS (SELECT 1 FROM ctr_customer c WHERE c.customer_id = b.customer_id
|
||||
<include refid="com.bms.ar.mapper.ArBillScopeMapper.customerScope"/>
|
||||
)
|
||||
</otherwise>
|
||||
</choose>
|
||||
</sql>
|
||||
<select id="countAccessibleBill" resultType="int">
|
||||
SELECT COUNT(*) FROM ar_bill b WHERE b.ar_bill_id = #{arBillId}
|
||||
<include refid="billScope"/>
|
||||
</select>
|
||||
<select id="selectAccessibleCustomer" resultMap="CustomerOption">
|
||||
SELECT c.customer_id, c.customer_code, c.customer_name FROM ctr_customer c
|
||||
WHERE c.customer_id = #{customerId} AND c.del_flag = '0'
|
||||
<include refid="customerScope"/>
|
||||
</select>
|
||||
<select id="selectCustomerOptions" resultMap="CustomerOption">
|
||||
SELECT c.customer_id, c.customer_code, c.customer_name FROM ctr_customer c WHERE c.del_flag = '0'
|
||||
<include refid="customerScope"/>
|
||||
<if test="keywords != null and !keywords.isEmpty()">
|
||||
AND (<foreach collection="keywords" item="keyword" separator=" OR ">
|
||||
(c.customer_name LIKE CONCAT('%', #{keyword}, '%') OR c.customer_code LIKE CONCAT('%', #{keyword}, '%')
|
||||
OR c.customer_code_dw LIKE CONCAT('%', #{keyword}, '%'))
|
||||
</foreach>)
|
||||
</if>
|
||||
ORDER BY c.customer_id DESC
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -1,5 +1,13 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listArBillCustomerOptions(query) {
|
||||
return request({ url: '/ar/bill/customer-options', method: 'get', params: query })
|
||||
}
|
||||
|
||||
export function listArBillGenerateCustomerOptions(query) {
|
||||
return request({ url: '/ar/bill/generate/customer-options', method: 'get', params: query })
|
||||
}
|
||||
|
||||
// 应收账单接口统一放在这里;生成和导出均为异步任务,提交接口不等待后台处理完成。
|
||||
export function listArBill(query) {
|
||||
return request({ url: '/ar/bill/list', method: 'get', params: query })
|
||||
|
||||
@@ -36,6 +36,11 @@ import { listCustomer, listCustomerOption } from '@/api/customersuppliermanageme
|
||||
import { listSupplier } from '@/api/customersuppliermanagement/supplierinfo/supplier.js'
|
||||
|
||||
const props = defineProps({
|
||||
// 业务页面可传入专用选项接口,默认行为保持不变。
|
||||
fetchOptions: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
},
|
||||
// 单选时为 String/Number;multiple 时为数组
|
||||
modelValue: {
|
||||
type: [String, Number, Array],
|
||||
@@ -143,9 +148,9 @@ const displayOptions = computed(() => {
|
||||
function fetchList(keyword = '') {
|
||||
if (!props.remoteSearch && loaded.value) return Promise.resolve(options.value)
|
||||
loading.value = true
|
||||
const api = !isCustomer.value
|
||||
const api = props.fetchOptions || (!isCustomer.value
|
||||
? listSupplier
|
||||
: (props.optionSelect ? listCustomerOption : listCustomer)
|
||||
: (props.optionSelect ? listCustomerOption : listCustomer))
|
||||
const currentRequest = ++requestSerial
|
||||
const query = String(keyword || '').trim()
|
||||
return api({
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-dialog v-model="dialogOpen" class="generate-dialog" title="生成应收账单" width="1200px" append-to-body destroy-on-close>
|
||||
<el-form ref="generateFormRef" :model="generateForm" :rules="generateRules" label-width="110px" class="generate-form">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8"><el-form-item label="客户" prop="customerId"><customer-select v-model="generateForm.customerId" placeholder="请输入客户名称或代码搜索" :remote-search="true" :page-size="20" @change="handleCustomerChange" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="客户" prop="customerId"><customer-select :fetch-options="listArBillGenerateCustomerOptions" v-model="generateForm.customerId" placeholder="请输入客户名称或代码搜索" :remote-search="true" :page-size="20" @change="handleCustomerChange" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="币种" prop="currency"><el-select v-model="generateForm.currency" placeholder="请选择币种" style="width: 100%;" @change="handleCurrencyChange"><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="账单截止日" prop="cutoffDate"><el-date-picker v-model="generateForm.cutoffDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择账单截止日" style="width: 100%;" @change="resetPreview" /></el-form-item></el-col>
|
||||
<el-col :span="8">
|
||||
@@ -146,7 +146,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="GenerateBillDialog">
|
||||
import { getDefaultArBankAccount, previewArBill, previewExcludedArBill, generateArBill } from '@/api/ar/bill'
|
||||
import { listArBillGenerateCustomerOptions, getDefaultArBankAccount, previewArBill, previewExcludedArBill, generateArBill } from '@/api/ar/bill'
|
||||
import { Refresh, Search, Select } from '@element-plus/icons-vue'
|
||||
import { AR_BILL_SELECTION_MODE } from '@/constants/ar/enums'
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<el-button type="warning" plain icon="Download" v-hasPermi="['ar:bill:export']" @click="handleExport">导出</el-button>
|
||||
</template>
|
||||
<template #search-customerId="{ model }">
|
||||
<customer-select v-model="model.customerId" placeholder="请输入客户名称或代码搜索" :remote-search="true" :page-size="20" />
|
||||
<customer-select :fetch-options="listArBillCustomerOptions" v-model="model.customerId" placeholder="请输入客户名称或代码搜索" :remote-search="true" :page-size="20" />
|
||||
</template>
|
||||
<template #col-status="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template>
|
||||
<template #col-totalAmount="{ row }"><span class="amount">{{ money(row.totalAmount) }}</span></template>
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
<script setup name="ArBill">
|
||||
import { saveAs } from 'file-saver'
|
||||
import { listArBill, getArBill, confirmArBill, voidArBill, exportArBill, getArBillExport, downloadArBillExport, getArBillGenerateTask, listActiveArBillGenerateTasks, cancelArBillGenerateTask, retryArBillGenerateTask } from '@/api/ar/bill'
|
||||
import { listArBillCustomerOptions, listArBill, getArBill, confirmArBill, voidArBill, exportArBill, getArBillExport, downloadArBillExport, getArBillGenerateTask, listActiveArBillGenerateTasks, cancelArBillGenerateTask, retryArBillGenerateTask } from '@/api/ar/bill'
|
||||
import BillDetail from './components/BillDetail.vue'
|
||||
import GenerateBillDialog from './components/GenerateBillDialog.vue'
|
||||
import { AR_BILL_STATUS, AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES } from '@/constants/ar/bill'
|
||||
|
||||
Reference in New Issue
Block a user