Merge branch 'test'

This commit is contained in:
caojia
2026-10-29 16:59:06 +08:00
76 changed files with 2557 additions and 261 deletions
+38
View File
@@ -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 解析,禁止非法日期自动滚动到其他日期。
*/
@@ -5,6 +5,7 @@ import java.util.List;
import com.bms.customer.customerinfo.domain.BmsCustomerAttachment;
import com.bms.customer.customerinfo.domain.BmsCustomerCompany;
import com.bms.customer.customerinfo.domain.BmsCustomerContact;
import com.bms.customer.customerinfo.vo.UnlinkedSalesCustomerVO;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
@@ -22,6 +23,7 @@ import com.bms.common.core.controller.BaseController;
import com.bms.common.core.domain.AjaxResult;
import com.bms.common.enums.BusinessType;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
import com.bms.customer.customerinfo.service.IBmsCustomerService;
import com.bms.common.utils.poi.ExcelUtil;
import com.bms.common.core.page.TableDataInfo;
@@ -51,6 +53,32 @@ public class BmsCustomerController extends BaseController
return getDataTable(list);
}
/** 查询未关联业务员的客户列表 */
@PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:list')")
@GetMapping("/unlinked-sales-user/list")
public TableDataInfo unlinkedSalesUserList()
{
startPage();
BmsCustomer query = new BmsCustomer();
List<UnlinkedSalesCustomerVO> list = bmsCustomerService.selectUnlinkedSalesUserCustomerList(query);
return getDataTable(list);
}
/**
* 客户下拉选项。
* 只返回客户ID、客户编码、客户名称,供各页面下拉框使用。
* 查询参数 keywords 按客户名称、客户编码、对外代码模糊匹配,多行之间为或者关系。
* 分页参数与原下拉一致,避免一次查出全部客户。
*/
// @PreAuthorize("@ss.hasPermi('customersupplier:customerinfo:list')")
@GetMapping("/optionselect")
public TableDataInfo optionselect(BmsCustomer query)
{
startPage();
List<BmsCustomerOption> list = bmsCustomerService.selectCustomerOptions(query);
return getDataTable(list);
}
/**
* 导出客户信息列表
*/
@@ -1,17 +1,29 @@
package com.bms.web.controller.masterdata;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.bms.common.annotation.Log;
import com.bms.common.core.controller.BaseController;
import com.bms.common.core.domain.AjaxResult;
import com.bms.common.core.page.TableDataInfo;
import com.bms.common.enums.BusinessType;
import com.bms.common.utils.poi.ExcelUtil;
import com.bms.masterdata.domain.ServiceChannel;
import com.bms.masterdata.service.IServiceChannelService;
/**
* 服务渠道(老系统镜像 srv_service_channel)
* 渠道管理(srv_service_channel)
* 下拉选项仍走 /optionselect,登录即可,供供应商绑定渠道使用。
*
* @author bms
* @date 2026-10-12
@@ -23,6 +35,32 @@ public class ServiceChannelController extends BaseController
@Autowired
private IServiceChannelService serviceChannelService;
/**
* 分页查询渠道列表。
* 渠道编码、渠道名称、服务商编码、服务商名称、目的港/目的地区域、发货人名称均可检索,名称类条件为模糊匹配。
*/
@PreAuthorize("@ss.hasPermi('data:serviceChannel:list')")
@GetMapping("/list")
public TableDataInfo list(ServiceChannel query)
{
startPage();
List<ServiceChannel> list = serviceChannelService.selectServiceChannelList(query);
return getDataTable(list);
}
/**
* 导出渠道列表,筛选条件与列表一致。
*/
@PreAuthorize("@ss.hasPermi('data:serviceChannel:export')")
@Log(title = "渠道管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, ServiceChannel query)
{
List<ServiceChannel> list = serviceChannelService.selectServiceChannelList(query);
ExcelUtil<ServiceChannel> util = new ExcelUtil<ServiceChannel>(ServiceChannel.class);
util.exportExcel(response, list, "渠道数据");
}
/**
* 服务渠道下拉选项(非分页)。
* 供供应商绑定渠道等场景选择渠道;不做权限控制(仍需登录)。
@@ -33,4 +71,63 @@ public class ServiceChannelController extends BaseController
List<ServiceChannel> list = serviceChannelService.selectServiceChannelList(query);
return success(list);
}
/**
* 查询渠道详情
*/
@PreAuthorize("@ss.hasPermi('data:serviceChannel:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable String id)
{
return success(serviceChannelService.selectServiceChannelById(id));
}
/**
* 新增渠道
*/
@PreAuthorize("@ss.hasPermi('data:serviceChannel:add')")
@Log(title = "渠道管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody ServiceChannel channel)
{
String username = getUsername();
channel.setCreateName(username);
channel.setModifier(username);
return toAjax(serviceChannelService.insertServiceChannel(channel));
}
/**
* 修改渠道
*/
@PreAuthorize("@ss.hasPermi('data:serviceChannel:edit')")
@Log(title = "渠道管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody ServiceChannel channel)
{
channel.setModifier(getUsername());
return toAjax(serviceChannelService.updateServiceChannel(channel));
}
/**
* 启用或停用渠道
*/
@PreAuthorize("@ss.hasPermi('data:serviceChannel:edit')")
@Log(title = "渠道管理", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody ServiceChannel channel)
{
channel.setModifier(getUsername());
return toAjax(serviceChannelService.updateServiceChannelStatus(channel));
}
/**
* 删除渠道。已被产品渠道或供应商引用时不允许删除。
*/
@PreAuthorize("@ss.hasPermi('data:serviceChannel:remove')")
@Log(title = "渠道管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids)
{
return toAjax(serviceChannelService.deleteServiceChannelByIds(ids));
}
}
@@ -227,6 +227,14 @@ public class PriceSetController extends BaseController
return success(priceSetService.selectDetailListByProduct(psvId, productCode));
}
/** All detail rows for a version (no page); batch prefetch instead of N byProduct calls. */
@PreAuthorize("@ss.hasPermi('price:set:list')")
@GetMapping("/detail/byVersion")
public AjaxResult detailByVersion(@RequestParam("psvId") Long psvId)
{
return success(priceSetService.selectDetailListByVersion(psvId));
}
/**
* 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区
*/
@@ -63,13 +63,13 @@ public class ProductController extends BaseController
}
/**
* 产品下拉选项(非分页)。
* 供计费列表、客户绑定等场景选择产品;不做权限控制(仍需登录)。
* Product dropdown (non-paged). Slim query, no list-page status refresh.
* Used by price-set / billing / customer bind; login required, no extra perm.
*/
@GetMapping("/optionselect")
public AjaxResult optionselect(BmsProduct product)
{
List<BmsProduct> list = productService.selectBmsProductList(product);
List<BmsProduct> list = productService.selectBmsProductOptions(product);
return success(list);
}
@@ -122,6 +122,7 @@ price.product.import.sheetNotFound=未找到「产品目录」sheet,请上传
price.product.import.noData=产品目录无有效数据行
price.product.import.catalogHeaderMissing=「产品目录」未找到表头「对接代码」,请在标题行下补齐表头(对接代码、产品名称、时效、走货属性、国家等)
price.product.import.countryCatalogHeaderMissing=「产品目录」未找到表头「产品名称」或「产品ID」,请确认是否为单国专线成本价表格式
price.product.import.singleCountryAsMulti=当前选择的是「全部导入(多国家价表)」,但上传的文件更像单国专线成本价表(价表无「国家」列)。请改用「按国家导入」,并选择目的国家后再上传。
price.product.import.regionEmpty=region 无可用国家数据,请先同步/维护国家表后再导入
price.product.import.partialFail=导入完成但存在失败项:新增 {0},跳过 {1};失败明细:{2}
price.product.import.effTooEarly=产品{0}({1})导入生效时间{2}早于该产品上一版生效时间{3},不允许导入
@@ -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;
}
}
@@ -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();
}
}
}
@@ -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);
}
}
}
@@ -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();
@@ -0,0 +1,22 @@
package com.bms.customer.customerinfo.domain;
import lombok.Data;
/**
* 客户下拉选项。
* 只携带下拉展示和回填需要的字段,不返回额度、账期等客户档案内容。
*
* @author bms
*/
@Data
public class BmsCustomerOption
{
/** 客户ID。收款、账单等已有下拉按主键选中,需一并返回 */
private Long customerId;
/** 客户编码 */
private String customerCode;
/** 客户名称 */
private String customerName;
}
@@ -1,10 +1,9 @@
package com.bms.customer.customerinfo.mapper;
import java.util.List;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerAttachment;
import com.bms.customer.customerinfo.domain.BmsCustomerCompany;
import com.bms.customer.customerinfo.domain.BmsCustomerContact;
import com.bms.customer.customerinfo.domain.*;
import com.bms.customer.customerinfo.vo.UnlinkedSalesCustomerVO;
import io.lettuce.core.dynamic.annotation.Param;
/**
@@ -34,6 +33,18 @@ public interface BmsCustomerMapper
*/
public List<BmsCustomer> selectBmsCustomerList(BmsCustomer bmsCustomer);
/**
* 查询客户下拉选项。
* 按客户名称、客户编码、对外代码模糊匹配,只返回下拉所需字段。
*
* @param query 下拉查询条件
* @return 客户下拉选项
*/
public List<BmsCustomerOption> selectCustomerOptions(BmsCustomer query);
/** 独立分页查询未关联业务员的客户,仅按 sales_user_id 为空筛选。 */
public List<UnlinkedSalesCustomerVO> selectUnlinkedSalesUserCustomerList(BmsCustomer query);
/** 校验业务员用户ID仍对应未删除的系统用户。 */
public int countValidSalesUserId(@org.apache.ibatis.annotations.Param("salesUserId") String salesUserId);
@@ -2,10 +2,12 @@ package com.bms.customer.customerinfo.service;
import java.util.List;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
import com.bms.customer.customerinfo.domain.BmsCustomerAttachment;
import com.bms.customer.customerinfo.domain.BmsCustomerCompany;
import com.bms.customer.customerinfo.domain.BmsCustomerContact;
import com.bms.customer.customerinfo.domain.BmsCustomerLog;
import com.bms.customer.customerinfo.vo.UnlinkedSalesCustomerVO;
/**
* 客户主Service接口
@@ -39,6 +41,18 @@ public interface IBmsCustomerService
*/
public List<BmsCustomer> selectBmsCustomerList(BmsCustomer bmsCustomer);
/** 独立查询未关联业务员的客户。 */
public List<UnlinkedSalesCustomerVO> selectUnlinkedSalesUserCustomerList(BmsCustomer query);
/**
* 查询客户下拉选项。
* 只返回客户ID、客户编码、客户名称;keywords 按客户名称、客户编码、对外代码模糊查询。
*
* @param query 下拉查询条件
* @return 客户下拉选项
*/
public List<BmsCustomerOption> selectCustomerOptions(BmsCustomer query);
/**
* 查询客户附件列表
*
@@ -73,14 +73,6 @@ public class BmsCustomerPriceSetServiceImpl implements IBmsCustomerPriceSetServi
{
throw new ServiceException("仅生效中的套餐允许绑定,当前套餐状态不可用");
}
if (bind.getPrBegindate() == null)
{
throw new ServiceException("绑定生效日期不能为空");
}
if (bind.getPrEnddate() != null && bind.getPrBegindate().after(bind.getPrEnddate()))
{
throw new ServiceException("失效日期不能早于生效日期");
}
if (bind.getBindPriority() == null || bind.getBindPriority() < 1)
{
bind.setBindPriority(1);
@@ -105,6 +97,15 @@ public class BmsCustomerPriceSetServiceImpl implements IBmsCustomerPriceSetServi
if (existing != null)
{
bind.setCustomerPricesetId(existing.getCustomerPricesetId());
// 弹框已不再采集生效/失效日期,未传入时保留库中原值
if (bind.getPrBegindate() == null)
{
bind.setPrBegindate(existing.getPrBegindate());
}
if (bind.getPrEnddate() == null)
{
bind.setPrEnddate(existing.getPrEnddate());
}
rows = bindMapper.updateBind(bind);
}
else
@@ -144,14 +145,6 @@ public class BmsCustomerPriceSetServiceImpl implements IBmsCustomerPriceSetServi
{
throw new ServiceException("仅生效中的套餐允许绑定,当前套餐状态不可用");
}
if (req.getPrBegindate() == null)
{
throw new ServiceException("绑定生效日期不能为空");
}
if (req.getPrEnddate() != null && req.getPrBegindate().after(req.getPrEnddate()))
{
throw new ServiceException("失效日期不能早于生效日期");
}
Integer priority = (req.getBindPriority() == null || req.getBindPriority() < 1) ? 1 : req.getBindPriority();
boolean overwrite = Boolean.TRUE.equals(req.getOverwriteExisting());
@@ -17,7 +17,9 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
import com.bms.customer.customerinfo.domain.BmsCustomerLog;
import com.bms.customer.customerinfo.vo.UnlinkedSalesCustomerVO;
import com.bms.customer.customerinfo.service.IBmsCustomerLogService;
import com.bms.customer.customerinfo.service.IBmsCustomerService;
import com.bms.customer.customermoney.service.ICustomerMoneyService;
@@ -89,6 +91,24 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
return bmsCustomerMapper.selectBmsCustomerList(bmsCustomer);
}
/** 查询未关联业务员客户,保留客户业务员维度的数据范围。 */
@Override
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id")
public List<UnlinkedSalesCustomerVO> selectUnlinkedSalesUserCustomerList(BmsCustomer query)
{
return bmsCustomerMapper.selectUnlinkedSalesUserCustomerList(query);
}
/**
* 客户下拉选项。数据权限与客户列表一致,只查下拉需要的字段。
*/
@Override
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id")
public List<BmsCustomerOption> selectCustomerOptions(BmsCustomer query)
{
return bmsCustomerMapper.selectCustomerOptions(query);
}
//查询附件子表数据
@Override
public List<BmsCustomerAttachment> selectBmsCustomerAttachmentList(BmsCustomerAttachment bmsCustomerAttachment) {
@@ -0,0 +1,16 @@
package com.bms.customer.customerinfo.vo;
import lombok.Data;
/** 未关联业务员客户弹窗专用列表响应。 */
@Data
public class UnlinkedSalesCustomerVO
{
private Long customerId;
private String customerCode;
private String customerName;
private String customerCodeDw;
private String customerType;
private String isEnable;
private String sourceSalesCode;
}
@@ -2,6 +2,7 @@ package com.bms.masterdata.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.bms.common.annotation.Excel;
import lombok.Data;
/**
@@ -17,53 +18,98 @@ public class ServiceChannel
private String id;
/** 渠道编码 */
@Excel(name = "渠道编码")
private String channelCode;
/** 渠道名称 */
@Excel(name = "渠道名称")
private String channelName;
/** 所属服务商编码 */
@Excel(name = "服务商编码")
private String serviceCode;
/** 所属服务商名称 */
@Excel(name = "服务商名称")
private String serviceName;
/** 是否启用(1启用 0停用) */
@Excel(name = "是否启用", readConverterExp = "1=启用,0=停用")
private String blOpen;
/** 是否支持多票(1是 0否) */
@Excel(name = "是否多票", readConverterExp = "1=是,0=否")
private String blMultiple;
@Excel(name = "货物类型")
private String cargoType;
@Excel(name = "付款方式")
private String payType;
@Excel(name = "发货人名称")
private String shipperName;
@Excel(name = "发货人ID")
private String shipperId;
@Excel(name = "轨迹查询类型")
private String queryType;
@Excel(name = "轨迹编码")
private String trajectoryCode;
@Excel(name = "末端服务商")
private String finalServiceName;
@Excel(name = "目的港/目的地区域")
private String desPort;
/** 重量类型 */
@Excel(name = "重量类型")
private String weightType;
/** 体积除数(抛货系数) */
@Excel(name = "抛货系数")
private Long dimDivisor;
@Excel(name = "计重规则")
private String weightRule;
@Excel(name = "发货类型")
private String shipmentType;
@Excel(name = "数据来源")
private String dataFrom;
@Excel(name = "备注")
private String remarks;
@Excel(name = "创建人")
private String createName;
private String createManCode;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "创建时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date createDate;
@Excel(name = "修改人")
private String modifier;
private String modifierCode;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "修改时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date modifyDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date gmtCreate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date gmtModified;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "同步时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date syncTime;
}
@@ -4,7 +4,7 @@ import java.util.List;
import com.bms.masterdata.domain.ServiceChannel;
/**
* 服务渠道镜像表查询(MASTER 数据源)
* 服务渠道 srv_service_channel
*
* @author bms
* @date 2026-10-12
@@ -12,7 +12,48 @@ import com.bms.masterdata.domain.ServiceChannel;
public interface ServiceChannelMapper
{
/**
* 查询服务渠道列表(支持渠道编码/名称/服务商编码/启用状态过滤,非分页)
* 查询服务渠道列表。
* 渠道名称、服务商名称、目的港/目的地区域、发货人名称为模糊匹配;编码同样按包含匹配,便于列表检索。
*/
List<ServiceChannel> selectServiceChannelList(ServiceChannel query);
/**
* 按主键查询渠道
*/
ServiceChannel selectServiceChannelById(String id);
/**
* 按渠道编码精确查询(唯一性校验)
*/
ServiceChannel selectServiceChannelByCode(String channelCode);
/**
* 产品渠道关系中引用该渠道编码的数量
*/
int countProductChannelByCode(String channelCode);
/**
* 供应商绑定中引用该渠道编码的数量
*/
int countSupplierByChannelCode(String channelCode);
/**
* 新增渠道
*/
int insertServiceChannel(ServiceChannel channel);
/**
* 修改渠道
*/
int updateServiceChannel(ServiceChannel channel);
/**
* 只更新启用状态
*/
int updateServiceChannelStatus(ServiceChannel channel);
/**
* 批量删除渠道
*/
int deleteServiceChannelByIds(String[] ids);
}
@@ -4,7 +4,7 @@ import java.util.List;
import com.bms.masterdata.domain.ServiceChannel;
/**
* 服务渠道镜像 服务层
* 服务渠道 服务层
*
* @author bms
* @date 2026-10-12
@@ -12,7 +12,32 @@ import com.bms.masterdata.domain.ServiceChannel;
public interface IServiceChannelService
{
/**
* 查询服务渠道列表(非分页,供供应商绑定渠道等下拉选择场景)
* 查询服务渠道列表(分页由调用方 startPage 控制;下拉场景不传分页)
*/
List<ServiceChannel> selectServiceChannelList(ServiceChannel query);
/**
* 查询渠道详情
*/
ServiceChannel selectServiceChannelById(String id);
/**
* 新增渠道
*/
int insertServiceChannel(ServiceChannel channel);
/**
* 修改渠道
*/
int updateServiceChannel(ServiceChannel channel);
/**
* 启用或停用渠道(只改 bl_open)
*/
int updateServiceChannelStatus(ServiceChannel channel);
/**
* 批量删除渠道。已被产品渠道或供应商引用时拒绝删除。
*/
int deleteServiceChannelByIds(String[] ids);
}
@@ -3,12 +3,15 @@ package com.bms.masterdata.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.StringUtils;
import com.bms.common.utils.uuid.IdUtils;
import com.bms.masterdata.domain.ServiceChannel;
import com.bms.masterdata.mapper.ServiceChannelMapper;
import com.bms.masterdata.service.IServiceChannelService;
/**
* 服务渠道镜像 服务实现
* 服务渠道 服务实现
*
* @author bms
* @date 2026-10-12
@@ -24,4 +27,169 @@ public class ServiceChannelServiceImpl implements IServiceChannelService
{
return serviceChannelMapper.selectServiceChannelList(query);
}
@Override
public ServiceChannel selectServiceChannelById(String id)
{
return serviceChannelMapper.selectServiceChannelById(id);
}
@Override
public int insertServiceChannel(ServiceChannel channel)
{
prepare(channel, true);
assertCodeUnique(channel);
channel.setId(IdUtils.fastSimpleUUID());
if (StringUtils.isEmpty(channel.getDataFrom()))
{
channel.setDataFrom("BMS");
}
return serviceChannelMapper.insertServiceChannel(channel);
}
@Override
public int updateServiceChannel(ServiceChannel channel)
{
if (channel == null || StringUtils.isEmpty(channel.getId()))
{
throw new ServiceException("渠道ID不能为空");
}
ServiceChannel existing = serviceChannelMapper.selectServiceChannelById(channel.getId());
if (existing == null)
{
throw new ServiceException("渠道不存在或已被删除");
}
prepare(channel, false);
assertCodeUnique(channel);
if (!StringUtils.equals(existing.getChannelCode(), channel.getChannelCode())
&& countUsage(existing.getChannelCode()) > 0)
{
throw new ServiceException("渠道「" + existing.getChannelName() + "」已被产品渠道或供应商引用,不能修改渠道编码");
}
return serviceChannelMapper.updateServiceChannel(channel);
}
@Override
public int updateServiceChannelStatus(ServiceChannel channel)
{
if (channel == null || StringUtils.isEmpty(channel.getId()))
{
throw new ServiceException("渠道ID不能为空");
}
if (!"0".equals(channel.getBlOpen()) && !"1".equals(channel.getBlOpen()))
{
throw new ServiceException("启用状态只能是启用或停用");
}
ServiceChannel existing = serviceChannelMapper.selectServiceChannelById(channel.getId());
if (existing == null)
{
throw new ServiceException("渠道不存在或已被删除");
}
return serviceChannelMapper.updateServiceChannelStatus(channel);
}
@Override
public int deleteServiceChannelByIds(String[] ids)
{
if (ids == null || ids.length == 0)
{
throw new ServiceException("请选择要删除的渠道");
}
for (String id : ids)
{
if (StringUtils.isEmpty(id))
{
continue;
}
ServiceChannel existing = serviceChannelMapper.selectServiceChannelById(id);
if (existing == null)
{
continue;
}
if (countUsage(existing.getChannelCode()) > 0)
{
throw new ServiceException("渠道「" + existing.getChannelName() + "」已被产品渠道或供应商引用,不能删除,请改为停用");
}
}
return serviceChannelMapper.deleteServiceChannelByIds(ids);
}
private void prepare(ServiceChannel channel, boolean creating)
{
if (channel == null)
{
throw new ServiceException("渠道信息不能为空");
}
channel.setChannelCode(StringUtils.trim(channel.getChannelCode()));
channel.setChannelName(StringUtils.trim(channel.getChannelName()));
channel.setServiceCode(trimToNull(channel.getServiceCode()));
channel.setServiceName(trimToNull(channel.getServiceName()));
channel.setShipperName(trimToNull(channel.getShipperName()));
channel.setDesPort(trimToNull(channel.getDesPort()));
if (StringUtils.isEmpty(channel.getChannelCode()))
{
throw new ServiceException("渠道编码不能为空");
}
if (channel.getChannelCode().length() > 64)
{
throw new ServiceException("渠道编码不能超过64个字符");
}
if (StringUtils.isEmpty(channel.getChannelName()))
{
throw new ServiceException("渠道名称不能为空");
}
if (channel.getChannelName().length() > 200)
{
throw new ServiceException("渠道名称不能超过200个字符");
}
if (StringUtils.isEmpty(channel.getBlOpen()))
{
if (creating)
{
channel.setBlOpen("1");
}
else
{
throw new ServiceException("请选择启用状态");
}
}
if (!"0".equals(channel.getBlOpen()) && !"1".equals(channel.getBlOpen()))
{
throw new ServiceException("启用状态只能是启用或停用");
}
if (StringUtils.isNotEmpty(channel.getBlMultiple())
&& !"0".equals(channel.getBlMultiple()) && !"1".equals(channel.getBlMultiple()))
{
throw new ServiceException("是否多票只能是是或否");
}
if (channel.getDimDivisor() != null && channel.getDimDivisor() < 0)
{
throw new ServiceException("抛货系数不能为负数");
}
}
private void assertCodeUnique(ServiceChannel channel)
{
ServiceChannel same = serviceChannelMapper.selectServiceChannelByCode(channel.getChannelCode());
if (same != null && !StringUtils.equals(same.getId(), channel.getId()))
{
throw new ServiceException("渠道编码「" + channel.getChannelCode() + "」已存在");
}
}
private int countUsage(String channelCode)
{
if (StringUtils.isEmpty(channelCode))
{
return 0;
}
return serviceChannelMapper.countProductChannelByCode(channelCode)
+ serviceChannelMapper.countSupplierByChannelCode(channelCode);
}
private String trimToNull(String value)
{
String text = StringUtils.trim(value);
return StringUtils.isEmpty(text) ? null : text;
}
}
@@ -29,11 +29,10 @@ public class PrcPriceSetVersion extends BaseEntity
/** 0 draft / 3 pending / 1 active / 2 expired */
private String status;
/**
* Create-price mode for this version:
* PULL = from product library,
* IMPORT = Excel with prior version preload,
* IMPORT_FRESH = Excel without prior version data.
* Column: price_build_mode
* Create-price mode for this version (column: price_build_mode).
* SALES: PULL = product library; IMPORT = Excel + prior version; IMPORT_FRESH = Excel blank.
* PRODUCT/REDLINE: IMPORT = copy from effect version; IMPORT_FRESH = rebuild (Excel).
* Legacy catalog: COPY → IMPORT, old IMPORT → IMPORT_FRESH (see migrate SQL).
*/
private String priceBuildMode;
/** 0 normal / 1 soft-deleted */
@@ -30,6 +30,9 @@ public interface BmsProductMapper
public List<BmsProduct> selectBmsProductList(BmsProduct product);
/** Dropdown options: slim columns, no country/surcharge count subqueries */
public List<BmsProduct> selectBmsProductOptions(BmsProduct product);
public int insertBmsProduct(BmsProduct product);
public int updateBmsProduct(BmsProduct product);
@@ -19,6 +19,9 @@ public interface IBmsProductService
public List<BmsProduct> selectBmsProductList(BmsProduct product);
/** Product dropdown options (no status refresh, no count subqueries). */
public List<BmsProduct> selectBmsProductOptions(BmsProduct product);
public int insertBmsProduct(BmsProduct product);
public int updateBmsProduct(BmsProduct product);
@@ -71,6 +71,11 @@ public interface IPrcPriceSetService
*/
public List<PrcPriceSetDetail> selectDetailListByProduct(Long psvId, String productCode);
/**
* Load all detail rows for a version (no pagination). For batch prefetch vs N byProduct calls.
*/
public List<PrcPriceSetDetail> selectDetailListByVersion(Long psvId);
/**
* 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区 列表。
* 返回 { products: [{code,name}], countries: [{code,name}], zones: [{code,name}] }
@@ -129,8 +129,7 @@ public class BmsProductServiceImpl implements IBmsProductService
@Override
public List<BmsProduct> selectBmsProductList(BmsProduct product)
{
productMapper.refreshStatusByDates();
countryMapper.refreshStatusByDates();
// Status refresh is done on activate / scheduled task; skip UPDATE on every list read.
if (product != null)
{
product.setStatus(normalizeStatusCode(product.getStatus()));
@@ -139,6 +138,17 @@ public class BmsProductServiceImpl implements IBmsProductService
return productMapper.selectBmsProductList(product);
}
@Override
public List<BmsProduct> selectBmsProductOptions(BmsProduct product)
{
if (product != null)
{
product.setStatus(normalizeStatusCode(product.getStatus()));
product.setEffectStatus(normalizeStatusCode(product.getEffectStatus()));
}
return productMapper.selectBmsProductOptions(product);
}
@Override
public int insertBmsProduct(BmsProduct product)
{
@@ -378,6 +388,7 @@ public class BmsProductServiceImpl implements IBmsProductService
{
throw new ServiceException(MessageUtils.message("price.product.import.sheetNotFound"));
}
assertNotSingleCountryQuoteForMultiImport(workbook);
List<BmsProduct> products = parseCatalog(catalog);
if (products.isEmpty())
@@ -678,6 +689,7 @@ public class BmsProductServiceImpl implements IBmsProductService
{
throw new ServiceException(MessageUtils.message("price.product.import.sheetNotFound"));
}
assertNotSingleCountryQuoteForMultiImport(workbook);
List<BmsProduct> products = dedupeCatalogProducts(parseCatalog(catalog));
if (products.isEmpty())
{
@@ -763,6 +775,7 @@ public class BmsProductServiceImpl implements IBmsProductService
}
if (out.isEmpty())
{
assertNotSingleCountryQuoteForMultiImport(workbook);
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
}
QuoteWorkbook parsed = new QuoteWorkbook();
@@ -778,6 +791,80 @@ public class BmsProductServiceImpl implements IBmsProductService
private Map<String, List<BmsProductCountry>> byCode;
}
/**
* Multi-country import expects price sheets with a "country" column.
* Single-country dedicated-line sheets use product-name + weight blocks instead.
*/
private void assertNotSingleCountryQuoteForMultiImport(Workbook workbook)
{
if (looksLikeSingleCountryQuote(workbook))
{
throw new ServiceException(MessageUtils.message("price.product.import.singleCountryAsMulti"));
}
}
/** Detect single-country cost-sheet layout (no country column on price sheets). */
private boolean looksLikeSingleCountryQuote(Workbook workbook)
{
if (workbook == null)
{
return false;
}
boolean sawCountryCol = false;
boolean sawCountryPriceBlock = false;
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
{
String sheetName = workbook.getSheetName(i);
if (shouldSkipCountryPriceSheet(sheetName))
{
continue;
}
Sheet sheet = workbook.getSheetAt(i);
if (sheet == null)
{
continue;
}
if (findHeaderCell(sheet, "\u56fd\u5bb6") != null)
{
sawCountryCol = true;
break;
}
int[] nameHeader = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0");
if (nameHeader == null)
{
continue;
}
Row head = sheet.getRow(nameHeader[0]);
if (head == null)
{
continue;
}
if (findColByHeader(head, "\u91cd\u91cf", 0) >= 0)
{
sawCountryPriceBlock = true;
}
}
if (sawCountryCol)
{
return false;
}
if (sawCountryPriceBlock)
{
return true;
}
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
if (catalog == null)
{
return false;
}
if (findHeaderCell(catalog, "\u5bf9\u63a5\u4ee3\u7801") != null)
{
return false;
}
return findHeaderCell(catalog, "\u4ea7\u54c1\u540d\u79f0") != null
|| findHeaderCell(catalog, "\u4ea7\u54c1ID") != null;
}
@Override
public Map<String, List<BmsProductCountry>> parseCountryQuoteExcelByProduct(byte[] bytes, String filename,
String countryCode) throws Exception
@@ -881,6 +881,18 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return selectDetailList(q);
}
@Override
public List<PrcPriceSetDetail> selectDetailListByVersion(Long psvId)
{
if (psvId == null)
{
return new ArrayList<PrcPriceSetDetail>();
}
PrcPriceSetDetail q = new PrcPriceSetDetail();
q.setPsvId(psvId);
return selectDetailList(q);
}
@Override
public Map<String, Object> selectDetailOptions(Long psvId)
{
@@ -102,7 +102,8 @@ public class CatalogVersionSupport
nv.setVersionNo("V" + next);
nv.setEffectiveTime(effectiveTime);
nv.setStatus(STATUS_DRAFT);
nv.setPriceBuildMode("IMPORT");
// IMPORT = inherit effect version; IMPORT_FRESH = empty rebuild (Excel)
nv.setPriceBuildMode(copyFromLatest ? "IMPORT" : "IMPORT_FRESH");
nv.setCreateBy("system");
versionMapper.insertPrcPriceSetVersion(nv);
@@ -122,7 +123,11 @@ public class CatalogVersionSupport
return nv;
}
/** Activate draft: set EFFECT, chain expire on previous EFFECT versions of same type. */
/**
* Activate draft/pending catalog version.
* IMPORT_FRESH (align sales): may start inside a prior window; forbid equal to any other
* version's effective/expire of the same price_type; then chain expire times and sync children.
*/
public void activateVersion(Long psvId, Date effectiveTime)
{
PrcPriceSetVersion ver = versionMapper.selectPrcPriceSetVersionById(psvId);
@@ -140,39 +145,165 @@ public class CatalogVersionSupport
{
throw new ServiceException("请选择生效时间");
}
Date prevEff = findPrevEffectiveTime(ver);
if (prevEff != null && eff.before(prevEff))
boolean importFresh = "IMPORT_FRESH".equalsIgnoreCase(StringUtils.trim(ver.getPriceBuildMode()));
if (importFresh)
{
throw new ServiceException("生效时间不能早于上一版本");
validateImportFreshActivateTime(ver, eff);
}
boolean future = eff.after(new Date());
if (!future)
else
{
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceType(ver.getPriceType());
q.setStatus(STATUS_EFFECT);
List<PrcPriceSetVersion> prevList = versionMapper.selectPrcPriceSetVersionList(q);
if (prevList != null)
Date prevEff = findPrevEffectiveTime(ver);
if (prevEff != null && eff.before(prevEff))
{
for (PrcPriceSetVersion prev : prevList)
{
if (prev.getPsvId().equals(psvId))
{
continue;
}
prev.setExpireTime(eff);
prev.setStatus(STATUS_EXPIRED);
versionMapper.updatePrcPriceSetVersion(prev);
}
throw new ServiceException("生效时间不能早于上一版本");
}
}
ver.setEffectiveTime(eff);
ver.setExpireTime(null);
ver.setStatus(future ? STATUS_PENDING : STATUS_EFFECT);
versionMapper.updatePrcPriceSetVersion(ver);
refreshCatalogStatuses(ver.getPriceType());
// Chain B.expire=A.eff, A.expire=next/B.oldExpire; refresh status + product/detail effect
chainAndRefreshCatalogStatuses(ver.getPriceType());
}
/**
* IMPORT_FRESH: may be earlier than prev-by-seq; must not equal any other version's eff/exp
* within the same PRODUCT/REDLINE type.
*/
private void validateImportFreshActivateTime(PrcPriceSetVersion version, Date effectiveTime)
{
if (version == null || effectiveTime == null || StringUtils.isEmpty(version.getPriceType()))
{
return;
}
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceType(version.getPriceType());
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
if (list == null || list.isEmpty())
{
return;
}
for (PrcPriceSetVersion v : list)
{
if (v == null)
{
continue;
}
if (version.getPsvId() != null && version.getPsvId().equals(v.getPsvId()))
{
continue;
}
if (sameTime(v.getEffectiveTime(), effectiveTime) || sameTime(v.getExpireTime(), effectiveTime))
{
throw new ServiceException("生效时间不能与已有版本的生效时间或失效时间相同");
}
}
}
/**
* Sort timed versions by effective_time, chain expire_time = next start, then set status
* and sync product/detail effect fields (same idea as sales refreshVersionStatus).
*/
private void chainAndRefreshCatalogStatuses(String priceType)
{
assertCatalogType(priceType);
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceType(priceType);
List<PrcPriceSetVersion> versions = versionMapper.selectPrcPriceSetVersionList(q);
if (versions == null || versions.isEmpty())
{
return;
}
versions.sort((a, b) -> {
Date ta = a.getEffectiveTime();
Date tb = b.getEffectiveTime();
if (ta == null && tb == null)
{
return Integer.compare(nullToZero(a.getVersionSeq()), nullToZero(b.getVersionSeq()));
}
if (ta == null)
{
return 1;
}
if (tb == null)
{
return -1;
}
int c = ta.compareTo(tb);
if (c != 0)
{
return c;
}
return Integer.compare(nullToZero(a.getVersionSeq()), nullToZero(b.getVersionSeq()));
});
Date now = new Date();
Long effectPsvId = null;
for (int i = 0; i < versions.size(); i++)
{
PrcPriceSetVersion v = versions.get(i);
Date nextStart = null;
for (int j = i + 1; j < versions.size(); j++)
{
Date t = versions.get(j).getEffectiveTime();
if (t != null)
{
nextStart = t;
break;
}
}
v.setExpireTime(nextStart);
if (v.getEffectiveTime() == null)
{
v.setStatus(STATUS_DRAFT);
continue;
}
if (v.getEffectiveTime().getTime() > now.getTime())
{
v.setStatus(STATUS_PENDING);
continue;
}
if (nextStart != null && nextStart.getTime() <= now.getTime())
{
v.setStatus(STATUS_EXPIRED);
continue;
}
v.setStatus(STATUS_EFFECT);
effectPsvId = v.getPsvId();
}
if (effectPsvId != null)
{
for (PrcPriceSetVersion v : versions)
{
if (STATUS_EFFECT.equals(v.getStatus())
&& v.getPsvId() != null
&& !v.getPsvId().equals(effectPsvId))
{
v.setStatus(STATUS_EXPIRED);
}
}
}
for (PrcPriceSetVersion v : versions)
{
PrcPriceSetVersion db = versionMapper.selectPrcPriceSetVersionById(v.getPsvId());
if (db == null)
{
continue;
}
boolean statusChanged = !String.valueOf(v.getStatus()).equals(String.valueOf(db.getStatus()));
boolean expireChanged = !sameTime(v.getExpireTime(), db.getExpireTime());
boolean effChanged = !sameTime(v.getEffectiveTime(), db.getEffectiveTime());
if (statusChanged || expireChanged || effChanged)
{
versionMapper.updatePrcPriceSetVersion(v);
}
}
productMapper.syncEffectByPriceType(priceType);
detailMapper.syncEffectByPriceType(priceType);
}
private static int nullToZero(Integer v)
{
return v == null ? 0 : v.intValue();
}
public List<PrcPriceSetVersion> listVersions(String priceType, String status)
@@ -345,14 +476,16 @@ public class CatalogVersionSupport
/**
* Prepare draft for catalog price create page.
* copyFromLatest=true: copy from current EFFECT version when draft is empty.
* copyFromLatest=false: empty draft (Excel import / rebuild).
* copyFromLatest=true → priceBuildMode=IMPORT: copy from current EFFECT version when draft is empty.
* copyFromLatest=false → priceBuildMode=IMPORT_FRESH: empty draft (Excel import / rebuild).
* Legacy values COPY/IMPORT are migrated by SQL to IMPORT/IMPORT_FRESH.
*/
public PrcPriceSetVersion prepareDraftVersion(String priceType, boolean copyFromLatest)
{
assertCatalogType(priceType);
List<PrcPriceSetVersion> drafts = listVersions(priceType, STATUS_DRAFT);
PrcPriceSetVersion draft;
String mode = copyFromLatest ? "IMPORT" : "IMPORT_FRESH";
if (drafts.isEmpty())
{
if (copyFromLatest)
@@ -364,7 +497,7 @@ public class CatalogVersionSupport
}
}
draft = ensureDraftVersion(priceType, null, copyFromLatest);
draft.setPriceBuildMode(copyFromLatest ? "COPY" : "IMPORT");
draft.setPriceBuildMode(mode);
versionMapper.updatePrcPriceSetVersion(draft);
return reloadCatalogVersion(draft.getPsvId(), priceType);
}
@@ -386,12 +519,12 @@ public class CatalogVersionSupport
productMapper.updatePriceTypeByPsvId(draft.getPsvId(), priceType);
detailMapper.updatePriceTypeByPsvId(draft.getPsvId(), priceType);
}
draft.setPriceBuildMode("COPY");
draft.setPriceBuildMode(mode);
versionMapper.updatePrcPriceSetVersion(draft);
}
else if (count == 0)
{
draft.setPriceBuildMode("IMPORT");
draft.setPriceBuildMode(mode);
versionMapper.updatePrcPriceSetVersion(draft);
}
return reloadCatalogVersion(draft.getPsvId(), priceType);
@@ -117,6 +117,11 @@ public class BmsRegionSyncServiceImpl implements IBmsRegionSyncService
if (existing != null)
{
region.setRegionId(existing.getRegionId());
// 已绑定到 OSS 的国旗不随外部图片地址覆盖,避免同步后国旗错位或丢失
if (isOwnFlag(existing.getFlagUrl()))
{
region.setFlagUrl(existing.getFlagUrl());
}
regionMapper.updateRegion(region);
updateCount++;
}
@@ -261,4 +266,11 @@ public class BmsRegionSyncServiceImpl implements IBmsRegionSyncService
return region;
}
/** 已经上传到本系统 OSS 的国旗地址,同步时保留。 */
private boolean isOwnFlag(String flagUrl)
{
return StringUtils.isNotEmpty(flagUrl)
&& (flagUrl.startsWith("oss://") || flagUrl.contains("/region/flag/"));
}
}
@@ -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') &gt;= 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') &gt;= 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') &lt;= 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') &lt;= 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 &gt;= #{billDateStart}</if>
<if test="billDateEnd != null">and bill_date &lt;= #{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 &gt;= #{query.billDateStart}</if>
<if test="query.billDateEnd != null">and bill_date &lt;= #{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 &gt; 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>
@@ -112,6 +112,55 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by c.customer_id desc
</select>
<select id="selectUnlinkedSalesUserCustomerList"
parameterType="com.bms.customer.customerinfo.domain.BmsCustomer"
resultType="com.bms.customer.customerinfo.vo.UnlinkedSalesCustomerVO">
select c.customer_id as customerId,
c.customer_code as customerCode,
c.customer_name as customerName,
c.customer_code_dw as customerCodeDw,
c.customer_type as customerType,
c.is_enable as isEnable,
c.source_sales_code as sourceSalesCode
from ctr_customer c
left join sys_user su
on su.del_flag = '0'
and cast(su.user_id as char) = c.sales_user_id
where c.del_flag = '0'
and (c.sales_user_id is null or trim(c.sales_user_id) = '')
${params.dataScope}
order by c.customer_id desc
</select>
<resultMap type="BmsCustomerOption" id="BmsCustomerOptionResult">
<result property="customerId" column="customer_id" />
<result property="customerCode" column="customer_code" />
<result property="customerName" column="customer_name" />
</resultMap>
<!-- 客户下拉:不关联账户、套餐,只取名称和编码;对外代码仅参与模糊条件 -->
<select id="selectCustomerOptions" parameterType="BmsCustomer" resultMap="BmsCustomerOptionResult">
select c.customer_id, c.customer_code, c.customer_name
from ctr_customer c
left join sys_user su
on su.del_flag = '0'
and cast(su.user_id as char) = c.sales_user_id
<where>
c.del_flag = '0'
<if test="keywordList != null and keywordList.size() > 0">
and (
<foreach collection="keywordList" item="kw" separator=" or ">
(c.customer_name like concat('%', #{kw}, '%')
or c.customer_code like concat('%', #{kw}, '%')
or c.customer_code_dw like concat('%', #{kw}, '%'))
</foreach>
)
</if>
${params.dataScope}
</where>
order by c.customer_id desc
</select>
<select id="countValidSalesUserId" resultType="int">
select count(1)
from sys_user
@@ -34,6 +34,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="modifyDate" column="modify_date"/>
<result property="gmtCreate" column="gmt_create"/>
<result property="gmtModified" column="gmt_modified"/>
<result property="syncTime" column="sync_time"/>
</resultMap>
<sql id="selectChannelVo">
@@ -55,15 +56,104 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND channel_name like concat('%', #{channelName}, '%')
</if>
<if test="serviceCode != null and serviceCode != ''">
AND service_code = #{serviceCode}
AND service_code like concat('%', #{serviceCode}, '%')
</if>
<if test="serviceName != null and serviceName != ''">
AND service_name like concat('%', #{serviceName}, '%')
</if>
<if test="desPort != null and desPort != ''">
AND des_port like concat('%', #{desPort}, '%')
</if>
<if test="shipperName != null and shipperName != ''">
AND shipper_name like concat('%', #{shipperName}, '%')
</if>
<if test="blOpen != null and blOpen != ''">
AND bl_open = #{blOpen}
</if>
</where>
order by service_code, channel_code
</select>
<select id="selectServiceChannelById" parameterType="String" resultMap="ServiceChannelResult">
<include refid="selectChannelVo"/>
where id = #{id}
</select>
<select id="selectServiceChannelByCode" parameterType="String" resultMap="ServiceChannelResult">
<include refid="selectChannelVo"/>
where channel_code = #{channelCode}
limit 1
</select>
<select id="countProductChannelByCode" parameterType="String" resultType="int">
select count(1) from srv_product_channel where channel_code = #{channelCode}
</select>
<select id="countSupplierByChannelCode" parameterType="String" resultType="int">
select count(1) from sup_supplier
where del_flag = '0'
and channel_ids is not null
and channel_ids != ''
and find_in_set(#{channelCode}, replace(channel_ids, ' ', ''))
</select>
<insert id="insertServiceChannel" parameterType="com.bms.masterdata.domain.ServiceChannel">
insert into srv_service_channel (
id, channel_code, channel_name, service_code, service_name,
bl_open, bl_multiple, cargo_type, pay_type, shipper_name, shipper_id,
query_type, trajectory_code, final_service_name, des_port,
weight_type, dim_divisor, weight_rule, shipment_type, data_from, remarks,
create_name, create_date, modifier, modify_date, gmt_create, gmt_modified
) values (
#{id}, #{channelCode}, #{channelName}, #{serviceCode}, #{serviceName},
#{blOpen}, #{blMultiple}, #{cargoType}, #{payType}, #{shipperName}, #{shipperId},
#{queryType}, #{trajectoryCode}, #{finalServiceName}, #{desPort},
#{weightType}, #{dimDivisor}, #{weightRule}, #{shipmentType}, #{dataFrom}, #{remarks},
#{createName}, sysdate(), #{modifier}, sysdate(), sysdate(), sysdate()
)
</insert>
<update id="updateServiceChannel" parameterType="com.bms.masterdata.domain.ServiceChannel">
update srv_service_channel
set channel_code = #{channelCode},
channel_name = #{channelName},
service_code = #{serviceCode},
service_name = #{serviceName},
bl_open = #{blOpen},
bl_multiple = #{blMultiple},
cargo_type = #{cargoType},
pay_type = #{payType},
shipper_name = #{shipperName},
shipper_id = #{shipperId},
query_type = #{queryType},
trajectory_code = #{trajectoryCode},
final_service_name = #{finalServiceName},
des_port = #{desPort},
weight_type = #{weightType},
dim_divisor = #{dimDivisor},
weight_rule = #{weightRule},
shipment_type = #{shipmentType},
data_from = #{dataFrom},
remarks = #{remarks},
modifier = #{modifier},
modify_date = sysdate(),
gmt_modified = sysdate()
where id = #{id}
</update>
<update id="updateServiceChannelStatus" parameterType="com.bms.masterdata.domain.ServiceChannel">
update srv_service_channel
set bl_open = #{blOpen},
modifier = #{modifier},
modify_date = sysdate(),
gmt_modified = sysdate()
where id = #{id}
</update>
<delete id="deleteServiceChannelByIds" parameterType="String">
delete from srv_service_channel where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
@@ -45,14 +45,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ifnull(p.is_del, '0') as is_del,
p.source_sheet, null as remark, null as tms_id,
v.created_by, p.create_time, v.updated_by, p.update_time,
(select count(distinct ifnull(c.country_code, c.country_name)) from prc_price_set_detail c
where ifnull(c.is_del, '0') = '0'
and (c.psp_id = p.psp_id or (c.psv_id = p.psv_id and c.product_code = p.product_code))
) as country_count,
(select count(*) from prc_surcharge_rel r
inner join prc_surcharge s on s.sc_id = r.sc_id and ifnull(s.is_del, '0') = '0'
where ifnull(r.is_del, '0') = '0'
and r.product_id = p.psp_id and r.price_type = 'PRODUCT') as surcharge_count
null as country_count, null as surcharge_count
from prc_price_set_product p
inner join prc_price_set_version v on v.psv_id = p.psv_id
</sql>
@@ -176,6 +169,66 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</choose>
</select>
<!-- Dropdown: same filters as list, without per-row country/surcharge count scans -->
<select id="selectBmsProductOptions" parameterType="BmsProduct" resultMap="BmsProductResult">
select p.psp_id as product_id, p.psv_id, p.price_type, p.product_code, p.product_name, p.service_level, p.cargo_attr,
p.dest_region, p.tax_mode,
p.effective_time as effective_date, p.expire_time as expire_date, p.status,
ifnull(p.is_del, '0') as is_del,
null as update_note, null as source_sheet, null as remark, null as tms_id,
null as created_by, p.create_time, null as updated_by, p.update_time,
null as country_count, null as surcharge_count
from prc_price_set_product p
inner join prc_price_set_version v on v.psv_id = p.psv_id
<where>
and p.price_type = 'PRODUCT'
and ifnull(p.is_del, '0') = '0'
<choose>
<when test="psvId != null">
AND p.psv_id = #{psvId}
</when>
<when test="statusGroup != null and statusGroup == 'effect'">
AND v.price_set_id is null AND v.status = '1'
</when>
<when test="statusGroup != null and statusGroup == 'pending'">
AND v.price_set_id is null AND v.status in ('0', '3')
</when>
<when test="statusGroup != null and statusGroup == 'expired'">
AND v.status = '2'
</when>
<otherwise>
AND p.psv_id = <include refid="latestProductPsv"/>
</otherwise>
</choose>
<if test="productCode != null and productCode != ''">
AND p.product_code like concat('%', #{productCode}, '%')
</if>
<if test="productName != null and productName != ''">
AND p.product_name like concat('%', #{productName}, '%')
</if>
<if test="taxMode != null and taxMode != ''">
AND p.tax_mode = #{taxMode}
</if>
<if test="status != null and status != ''">
AND v.status = #{status}
</if>
<if test="effectStatus != null and effectStatus != ''">
AND v.status = #{effectStatus}
</if>
<if test="statusGroup != null and statusGroup == 'active'">
AND v.status in ('0', '1')
</if>
<if test="cargoAttr != null and cargoAttr != ''">
AND p.cargo_attr like concat('%', #{cargoAttr}, '%')
</if>
<if test="params != null and params.keyword != null and params.keyword != ''">
AND (p.product_code like concat('%', #{params.keyword}, '%')
OR p.product_name like concat('%', #{params.keyword}, '%'))
</if>
</where>
order by p.product_code asc, p.psp_id desc
</select>
<insert id="insertBmsProduct" parameterType="BmsProduct" useGeneratedKeys="true" keyProperty="productId">
insert into prc_price_set_product (
price_type, psv_id, product_code, product_name, service_level, cargo_attr,
+1 -1
View File
@@ -1298,7 +1298,7 @@ CREATE TABLE `prc_price_set_version` (
`effective_time` datetime DEFAULT NULL COMMENT '生效时间',
`expire_time` datetime DEFAULT NULL COMMENT '失效时间(空=长期;下一版本启动时写入该启动时间)',
`status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '0 draft 3 pending 1 effect 2 expired',
`price_build_mode` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建价格方式 PULL=拉取产品库 IMPORT=Excel继承上一版 IMPORT_FRESH=Excel重新建价',
`price_build_mode` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建价格方式 SALES:PULL/IMPORT/IMPORT_FRESH; PRODUCT/REDLINE:IMPORT=沿用生效版 IMPORT_FRESH=重新建价',
`is_del` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '是否删除 0未删除 1已删除',
`created_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
+251
View File
@@ -0,0 +1,251 @@
-- 区域国旗全部改为 OSS 地址
-- 上次约 104 条能看到图,是因为只有这些行原来写了 flagcdn 地址,其余 flag_url 为空
-- 本次只按国家两位码更新,中文名写法不同也不会漏
SET NAMES utf8mb4;
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ad.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AD';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ae.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/af.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AF';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ag.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ai.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AI';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/al.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AL';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/am.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ao.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ar.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/as.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AS';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/at.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AT';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/au.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AU';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/aw.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AW';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ax.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AX';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/az.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'AZ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ba.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bb.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BB';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bd.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BD';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/be.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bf.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BF';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bg.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bh.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BH';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bi.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BI';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bj.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BJ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bl.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BL';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bm.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bn.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bo.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bq.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BQ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/br.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bs.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BS';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bt.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BT';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bw.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BW';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/by.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BY';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/bz.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'BZ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ca.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cc.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CC';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cd.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CD';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cf.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CF';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cg.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ch.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CH';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ci.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CI';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ck.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CK';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cl.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CL';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cm.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cn.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/co.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cu.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CU';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cv.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CV';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cw.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CW';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cx.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CX';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cy.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CY';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/cz.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'CZ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/de.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'DE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/dj.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'DJ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/dk.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'DK';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/dm.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'DM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/do.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'DO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/dz.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'DZ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ec.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'EC';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ee.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'EE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/eg.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'EG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/eh.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'EH';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/er.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'ER';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/es.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'ES';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/et.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'ET';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/fi.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'FI';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/fj.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'FJ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/fk.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'FK';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/fo.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'FO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/fr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'FR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ga.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gb.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GB';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gd.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GD';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ge.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gf.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GF';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gg.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gh.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GH';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gi.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GI';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gl.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GL';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gm.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gn.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gp.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GP';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gq.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GQ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gs.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GS';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gt.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GT';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gu.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GU';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gw.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GW';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/gy.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'GY';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/hk.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'HK';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/hn.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'HN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/hr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'HR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ht.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'HT';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/hu.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'HU';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/id.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'ID';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ie.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'IE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/il.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'IL';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/im.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'IM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/in.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'IN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/io.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'IO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/iq.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'IQ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ir.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'IR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/is.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'IS';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/it.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'IT';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/je.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'JE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/jm.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'JM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/jo.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'JO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/jp.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'JP';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ke.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/kg.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/kh.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KH';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ki.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KI';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/km.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/kn.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/kp.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KP';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/kr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/kw.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KW';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ky.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KY';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/kz.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'KZ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/la.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/lb.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LB';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/lc.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LC';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/li.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LI';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/lk.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LK';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/lr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ls.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LS';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/lt.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LT';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/lu.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LU';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/lv.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LV';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ly.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'LY';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ma.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mc.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MC';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/md.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MD';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/me.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'ME';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mf.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MF';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mg.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mh.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MH';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mk.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MK';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ml.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'ML';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mm.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mn.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mo.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mp.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MP';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mq.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MQ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ms.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MS';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mt.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MT';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mu.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MU';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mv.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MV';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mw.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MW';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mx.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MX';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/my.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MY';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/mz.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'MZ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/na.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/nc.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NC';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ne.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/nf.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NF';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ng.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ni.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NI';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/nl.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NL';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/no.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/np.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NP';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/nr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/nu.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NU';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/nz.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'NZ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/om.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'OM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pa.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pe.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pf.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PF';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pg.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ph.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PH';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pk.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PK';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pl.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PL';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pm.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pn.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ps.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PS';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pt.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PT';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/pw.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PW';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/py.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'PY';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/qa.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'QA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/re.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'RE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ro.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'RO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/rs.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'RS';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ru.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'RU';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/rw.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'RW';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sa.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sb.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SB';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sc.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SC';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sd.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SD';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/se.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sg.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sh.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SH';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/si.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SI';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sj.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SJ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sk.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SK';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sl.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SL';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sm.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sn.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/so.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ss.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SS';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/st.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'ST';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sv.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SV';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sx.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SX';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sy.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SY';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/sz.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'SZ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tc.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TC';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/td.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TD';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tf.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TF';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tg.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/th.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TH';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tj.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TJ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tk.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TK';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tl.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TL';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tm.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tn.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/to.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TO';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tr.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TR';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tt.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TT';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tv.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TV';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tw.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TW';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/tz.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'TZ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ua.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'UA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ug.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'UG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/um.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'UM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/us.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'US';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/uy.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'UY';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/uz.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'UZ';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/va.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'VA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/vc.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'VC';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ve.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'VE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/vg.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'VG';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/vi.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'VI';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/vn.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'VN';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/vu.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'VU';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/wf.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'WF';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ws.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'WS';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/xk.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'XK';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/ye.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'YE';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/yt.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'YT';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/za.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'ZA';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/zm.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'ZM';
UPDATE region SET flag_url = 'https://oss.lionquexpress.com/bms/prod/region/flag/zw.png', update_time = sysdate() WHERE del_flag = '0' AND country_code_2 = 'ZW';
@@ -0,0 +1,68 @@
-- 数据管理 - 渠道管理
-- 权限:data:serviceChannel:list|query|add|edit|remove|export
-- 启用/停用走修改权限 data:serviceChannel:edit
SET NAMES utf8mb4;
SET @parentId := (
SELECT menu_id FROM sys_menu
WHERE path = 'data' AND menu_type = 'M' AND parent_id = 0
ORDER BY menu_id ASC LIMIT 1
);
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT 2502, N'渠道管理', @parentId, 3, 'serviceChannel', 'data/serviceChannel/index', '', 'ServiceChannel', 1, 0, 'C', '0', '0', 'data:serviceChannel:list', 'guide', 'admin', sysdate(), N'服务渠道维护,数据表 srv_service_channel'
FROM DUAL WHERE @parentId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2502 OR perms = 'data:serviceChannel:list');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2503, N'查询', 2502, 1, '', NULL, 1, 0, 'F', '0', '0', 'data:serviceChannel:query', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2503 OR perms = 'data:serviceChannel:query');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2504, N'新增', 2502, 2, '', NULL, 1, 0, 'F', '0', '0', 'data:serviceChannel:add', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2504 OR perms = 'data:serviceChannel:add');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2505, N'修改', 2502, 3, '', NULL, 1, 0, 'F', '0', '0', 'data:serviceChannel:edit', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2505 OR perms = 'data:serviceChannel:edit');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2506, N'删除', 2502, 4, '', NULL, 1, 0, 'F', '0', '0', 'data:serviceChannel:remove', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2506 OR perms = 'data:serviceChannel:remove');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT 2507, N'导出', 2502, 5, '', NULL, 1, 0, 'F', '0', '0', 'data:serviceChannel:export', '#', 'admin', sysdate()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2507 OR perms = 'data:serviceChannel:export');
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, m.menu_id FROM (
SELECT 2502 AS menu_id UNION SELECT 2503 UNION SELECT 2504 UNION SELECT 2505 UNION SELECT 2506 UNION SELECT 2507
) m
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
-- 已有「数据管理-区域管理」权限的角色,同步授予对应的渠道管理权限
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT rm.role_id, dst.menu_id
FROM sys_role_menu rm
JOIN sys_menu src ON src.menu_id = rm.menu_id AND src.perms = 'data:region:list'
JOIN sys_menu dst ON dst.perms IN ('data:serviceChannel:list', 'data:serviceChannel:query', 'data:serviceChannel:export')
WHERE NOT EXISTS (
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = dst.menu_id
);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT rm.role_id, dst.menu_id
FROM sys_role_menu rm
JOIN sys_menu src ON src.menu_id = rm.menu_id AND src.perms = 'data:region:edit'
JOIN sys_menu dst ON dst.perms IN ('data:serviceChannel:add', 'data:serviceChannel:edit')
WHERE NOT EXISTS (
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = dst.menu_id
);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT rm.role_id, dst.menu_id
FROM sys_role_menu rm
JOIN sys_menu src ON src.menu_id = rm.menu_id AND src.perms = 'data:region:remove'
JOIN sys_menu dst ON dst.perms = 'data:serviceChannel:remove'
WHERE NOT EXISTS (
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = dst.menu_id
);
@@ -0,0 +1,15 @@
-- Catalog (PRODUCT / REDLINE) price_build_mode rename:
-- COPY → IMPORT (按现有生效版本建价)
-- IMPORT → IMPORT_FRESH (重新建产品价)
-- Order matters: migrate old IMPORT first, then COPY.
SET NAMES utf8mb4;
UPDATE prc_price_set_version
SET price_build_mode = 'IMPORT_FRESH'
WHERE price_type IN ('PRODUCT', 'REDLINE')
AND UPPER(IFNULL(price_build_mode, '')) = 'IMPORT';
UPDATE prc_price_set_version
SET price_build_mode = 'IMPORT'
WHERE price_type IN ('PRODUCT', 'REDLINE')
AND UPPER(IFNULL(price_build_mode, '')) = 'COPY';
@@ -1,3 +1,3 @@
-- Add create-price mode on package version: PULL / IMPORT
ALTER TABLE prc_price_set_version
ADD COLUMN price_build_mode varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建价格方式 PULL=拉取产品库 IMPORT=Excel继承上一版 IMPORT_FRESH=Excel重新建价' AFTER status;
ADD COLUMN price_build_mode varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建价格方式 SALES:PULL/IMPORT/IMPORT_FRESH; PRODUCT/REDLINE:IMPORT=沿用生效版 IMPORT_FRESH=重新建价' AFTER status;
+8
View File
@@ -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 })
@@ -9,6 +9,25 @@ export function listCustomer(query) {
})
}
// 分页查询未关联业务员的客户
export function listUnlinkedSalesUserCustomers(query) {
return request({
url: '/customersupplier/customerinfo/unlinked-sales-user/list',
method: 'get',
params: query
})
}
// 客户下拉选项:只返回客户ID、客户编码、客户名称
// keywords 按客户名称、客户编码、对外代码模糊查询
export function listCustomerOption(query) {
return request({
url: '/customersupplier/customerinfo/optionselect',
method: 'get',
params: query
})
}
// 查询客户主详细
export function getCustomer(customerId) {
return request({
@@ -1,5 +1,14 @@
import request from '@/utils/request'
/** 分页查询渠道列表 */
export function listServiceChannel(query) {
return request({
url: '/masterdata/servicechannel/list',
method: 'get',
params: query
})
}
/** 服务渠道下拉选项(非分页,来源 srv_service_channel 老系统镜像) */
export function listChannelOptions(query) {
return request({
@@ -8,3 +17,46 @@ export function listChannelOptions(query) {
params: query
})
}
/** 查询渠道详情 */
export function getServiceChannel(id) {
return request({
url: '/masterdata/servicechannel/' + id,
method: 'get'
})
}
/** 新增渠道 */
export function addServiceChannel(data) {
return request({
url: '/masterdata/servicechannel',
method: 'post',
data: data
})
}
/** 修改渠道 */
export function updateServiceChannel(data) {
return request({
url: '/masterdata/servicechannel',
method: 'put',
data: data
})
}
/** 启用或停用渠道 */
export function changeServiceChannelStatus(id, blOpen) {
return request({
url: '/masterdata/servicechannel/changeStatus',
method: 'put',
data: { id, blOpen }
})
}
/** 删除渠道 */
export function delServiceChannel(ids) {
return request({
url: '/masterdata/servicechannel/' + ids,
method: 'delete'
})
}
+9
View File
@@ -198,6 +198,15 @@ export function listSetDetailByProduct(psvId, productCode) {
})
}
/** All detail rows for a version (no pagination); one call instead of N byProduct. */
export function listSetDetailByVersion(psvId) {
return request({
url: '/price/set/detail/byVersion',
method: 'get',
params: { psvId }
})
}
/** 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区 */
export function listSetDetailOptions(psvId) {
return request({
@@ -32,10 +32,15 @@
</template>
<script setup name="CustomerSelect">
import { listCustomer } from '@/api/customersuppliermanagement/customerinfo/customer.js'
import { listCustomer, listCustomerOption } from '@/api/customersuppliermanagement/customerinfo/customer.js'
import { listSupplier } from '@/api/customersuppliermanagement/supplierinfo/supplier.js'
const props = defineProps({
// 业务页面可传入专用选项接口,默认行为保持不变。
fetchOptions: {
type: Function,
default: undefined
},
// 单选时为 String/Number;multiple 时为数组
modelValue: {
type: [String, Number, Array],
@@ -95,6 +100,11 @@ const props = defineProps({
remoteSearch: {
type: Boolean,
default: false
},
// 为 true 时客户下拉走专用选项接口,默认仍走客户列表接口
optionSelect: {
type: Boolean,
default: false
}
})
@@ -138,7 +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 ? listCustomer : listSupplier
const api = props.fetchOptions || (!isCustomer.value
? listSupplier
: (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'
+2 -2
View File
@@ -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'
@@ -0,0 +1,86 @@
<template>
<el-dialog
:model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)"
title="未关联业务员客户"
width="min(1100px, 96vw)"
append-to-body
destroy-on-close
@open="handleOpen"
>
<el-table v-loading="loading" :data="customerList" border row-key="customerId" empty-text="暂无未关联业务员的客户">
<el-table-column label="客户编码" prop="customerCode" min-width="120" align="center" />
<el-table-column label="客户名称" prop="customerName" min-width="240" show-overflow-tooltip />
<el-table-column label="对外代码" prop="customerCodeDw" min-width="140" align="center" />
<el-table-column label="源业务员工号" prop="sourceSalesCode" min-width="150" align="center" />
<el-table-column label="业务类型" prop="customerType" width="110" align="center">
<template #default="scope">
<dict-tag :options="customer_type" :value="scope.row.customerType" />
</template>
</el-table-column>
<el-table-column label="启用状态" prop="isEnable" width="110" align="center">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.isEnable" />
</template>
</el-table-column>
<el-table-column label="业务员" width="120" align="center">
<template #default>
<span class="unlinked-label">未关联</span>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="pageNum"
v-model:limit="pageSize"
@pagination="getList"
/>
<template #footer>
<el-button @click="$emit('update:modelValue', false)">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
import { listUnlinkedSalesUserCustomers } from '@/api/customersuppliermanagement/customerinfo/customer'
defineProps({
modelValue: { type: Boolean, default: false }
})
defineEmits(['update:modelValue'])
const { sys_normal_disable, customer_type } = useDict('sys_normal_disable', 'customer_type')
const loading = ref(false)
const customerList = ref([])
const total = ref(0)
const pageNum = ref(1)
const pageSize = ref(10)
function handleOpen() {
pageNum.value = 1
getList()
}
function getList() {
loading.value = true
listUnlinkedSalesUserCustomers({
pageNum: pageNum.value,
pageSize: pageSize.value
}).then(response => {
customerList.value = response.rows || []
total.value = response.total || 0
}).finally(() => {
loading.value = false
})
}
</script>
<style scoped>
.unlinked-label {
color: var(--el-color-warning);
}
</style>
@@ -661,7 +661,6 @@
filterable
placeholder="请选择生效中的套餐"
style="width: 100%;"
@change="onPriceSetChange"
>
<el-option
v-for="opt in priceSetOptions"
@@ -685,30 +684,6 @@
<el-input-number v-model="priceSetForm.bindPriority" :min="1" controls-position="right" style="width: 100%;" />
<div class="pane-tip">数字越小优先级越高,1 为最高</div>
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="生效日期" prop="prBegindate">
<el-date-picker
v-model="priceSetForm.prBegindate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="失效日期" prop="prEnddate">
<el-date-picker
v-model="priceSetForm.prEnddate"
type="date"
value-format="YYYY-MM-DD"
placeholder="长期有效"
style="width: 100%;"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="priceSetNote">
<el-input v-model="priceSetForm.priceSetNote" type="textarea" :rows="2" maxlength="100" show-word-limit placeholder="绑定备注(选填)" />
</el-form-item>
@@ -835,8 +810,7 @@ const priceSetForm = ref({})
const priceSetRules = {
priceSetId: [{ required: true, message: '请选择价格套餐', trigger: 'change' }],
bindPriority: [{ required: true, message: '优先级不能为空', trigger: 'blur' }],
prBegindate: [{ required: true, message: '绑定生效日期不能为空', trigger: 'change' }]
bindPriority: [{ required: true, message: '优先级不能为空', trigger: 'blur' }]
}
const selectedPriceSet = computed(() =>
@@ -871,26 +845,18 @@ function openPriceSetDialog(row) {
ensurePriceSetOptions()
priceSetForm.value = row
? { ...row }
: { priceSetId: null, bindPriority: 1, prBegindate: null, prEnddate: null, priceSetNote: null }
: { priceSetId: null, bindPriority: 1, priceSetNote: null }
priceSetDialogTitle.value = row || boundPriceSets.value.length ? '更换套餐' : '绑定套餐'
priceSetDialogVisible.value = true
nextTick(() => proxy.$refs['priceSetRef'] && proxy.$refs['priceSetRef'].clearValidate())
}
/** 选择套餐后,生效日期未填时默认今天 */
function onPriceSetChange(priceSetId) {
if (!priceSetId || priceSetForm.value.prBegindate) return
priceSetForm.value.prBegindate = proxy.parseTime(new Date(), '{y}-{m}-{d}')
}
function submitPriceSetForm() {
proxy.$refs['priceSetRef'].validate(valid => {
if (!valid) return
if (priceSetForm.value.prEnddate && priceSetForm.value.prBegindate > priceSetForm.value.prEnddate) {
proxy.$modal.msgWarning('失效日期不能早于生效日期')
return
}
const payload = { ...priceSetForm.value, customerId: form.value.customerId }
delete payload.prBegindate
delete payload.prEnddate
priceSetSaving.value = true
saveCustomerPriceSet(payload).then(() => {
proxy.$modal.msgSuccess('保存成功')
@@ -114,6 +114,15 @@
v-hasPermi="['customersupplier:customerinfo:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="User"
@click="unlinkedSalesDialogVisible = true"
v-hasPermi="['customersupplier:customerinfo:list']"
>未关联业务员</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
@@ -252,20 +261,6 @@
/>
</el-select>
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="生效日期" prop="prBegindate">
<el-date-picker v-model="batchForm.prBegindate" type="date" value-format="YYYY-MM-DD"
placeholder="选择日期" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="失效日期" prop="prEnddate">
<el-date-picker v-model="batchForm.prEnddate" type="date" value-format="YYYY-MM-DD"
placeholder="长期有效" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="优先级">
<el-input-number v-model="batchForm.bindPriority" :min="1" controls-position="right" style="width: 100%;" />
<div class="batch-tip">数字越小优先级越高,1 为最高</div>
@@ -302,6 +297,8 @@
:customer-name="billCustomer.customerName"
/>
<customer-unlinked-sales-dialog v-model="unlinkedSalesDialogVisible" />
<!-- 右键复制列值 -->
<table-copy-menu
ref="copyMenuRef"
@@ -319,6 +316,7 @@ import { useProducts, parseProductCodes } from '@/components/ProductSelect/usePr
import { useUsers } from '@/components/UserSelect/useUsers'
import CustomerLogDialog from './components/CustomerLogDialog.vue'
import CustomerBillListDialog from './customermoney/CustomerBillListDialog.vue'
import CustomerUnlinkedSalesDialog from './components/CustomerUnlinkedSalesDialog.vue'
import { useColumnSettings } from '@/utils/columnSettings'
const { proxy } = getCurrentInstance()
@@ -531,6 +529,7 @@ function handleCustomerMoney(row) {
/** 在客户信息列表内打开只读账单弹窗 */
const billDialogVisible = ref(false)
const billCustomer = ref({})
const unlinkedSalesDialogVisible = ref(false)
function handleViewBills(row) {
if (!row || !row.customerId) {
proxy.$modal.msgWarning('客户信息无效,无法查看账单')
@@ -550,8 +549,7 @@ const batchFormRef = ref(null)
const priceSetOptions = ref([])
const batchForm = ref({})
const batchRules = {
priceSetId: [{ required: true, message: '请选择价格套餐', trigger: 'change' }],
prBegindate: [{ required: true, message: '绑定生效日期不能为空', trigger: 'change' }]
priceSetId: [{ required: true, message: '请选择价格套餐', trigger: 'change' }]
}
/** 勾选客户中已绑定套餐的数量 */
@@ -566,8 +564,6 @@ function handleBatchBindPriceSet() {
batchForm.value = {
priceSetId: null,
bindPriority: 1,
prBegindate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
prEnddate: null,
priceSetNote: null,
overwriteExisting: false
}
@@ -583,10 +579,6 @@ function handleBatchBindPriceSet() {
function submitBatchBind() {
batchFormRef.value.validate(valid => {
if (!valid) return
if (batchForm.value.prEnddate && batchForm.value.prBegindate > batchForm.value.prEnddate) {
proxy.$modal.msgWarning('失效日期不能早于生效日期')
return
}
const total = selectedRows.value.length
const bound = boundCount.value
const setName = (priceSetOptions.value.find(o => o.priceSetId === batchForm.value.priceSetId) || {}).priceSetName || ''
@@ -602,8 +594,6 @@ function submitBatchBind() {
customerIds: selectedRows.value.map(r => r.customerId),
priceSetId: batchForm.value.priceSetId,
bindPriority: batchForm.value.bindPriority,
prBegindate: batchForm.value.prBegindate,
prEnddate: batchForm.value.prEnddate,
priceSetNote: batchForm.value.priceSetNote,
overwriteExisting: batchForm.value.overwriteExisting
})
+17 -2
View File
@@ -70,7 +70,8 @@
</template>
<template #col-flagEmoji="{ row }">
<span style="font-size: 18px">{{ row.flagEmoji }}</span>
<img v-if="row.flagUrl" :src="row.flagUrl" alt="" class="region-flag" />
<span v-else class="region-flag-emoji">{{ row.flagEmoji }}</span>
</template>
<template #col-action="{ row }">
@@ -267,7 +268,7 @@ const searchColumns = [
// 表格列配置
const columns = [
{ label: "区域ID", prop: "regionId", align: "center", width: 80 },
{ label: "国旗", prop: "flagEmoji", align: "center", width: 60 },
{ label: "国旗", prop: "flagEmoji", align: "center", width: 70 },
{ label: "中文名", prop: "countryNameCn", align: "center" },
{ label: "英文名", prop: "countryNameEn", align: "center" },
{ label: "国家代码", prop: "countryCode2", align: "center", width: 80 },
@@ -475,3 +476,17 @@ onBeforeUnmount(() => {
}
})
</script>
<style scoped>
.region-flag {
width: 30px;
height: 30px;
object-fit: contain;
display: block;
margin: 0 auto;
}
.region-flag-emoji {
font-size: 18px;
line-height: 30px;
}
</style>
@@ -0,0 +1,304 @@
<template>
<div class="app-container">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="id"
selection
>
<template #toolbar="{ selection, ids }">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['data:serviceChannel:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="selection.length !== 1" @click="handleUpdate(selection[0])" v-hasPermi="['data:serviceChannel:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="!ids.length" @click="handleDelete(ids)" v-hasPermi="['data:serviceChannel:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['data:serviceChannel:export']">导出</el-button>
</el-col>
</template>
<template #col-blOpen="{ row }">
<el-switch
v-model="row.blOpen"
active-value="1"
inactive-value="0"
@change="handleStatusChange(row)"
v-hasPermi="['data:serviceChannel:edit']"
/>
</template>
<template #col-blMultiple="{ row }">
<span>{{ row.blMultiple === '1' ? '是' : row.blMultiple === '0' ? '否' : '-' }}</span>
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['data:serviceChannel:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete([row.id])" v-hasPermi="['data:serviceChannel:remove']">删除</el-button>
</template>
</basic-table>
<el-dialog :title="title" v-model="open" width="760px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="渠道编码" prop="channelCode">
<el-input v-model="form.channelCode" placeholder="请输入渠道编码" maxlength="64" :disabled="!!form.id" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="渠道名称" prop="channelName">
<el-input v-model="form.channelName" placeholder="请输入渠道名称" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="服务商编码" prop="serviceCode">
<el-input v-model="form.serviceCode" placeholder="所属服务商编码" maxlength="32" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="服务商名称" prop="serviceName">
<el-input v-model="form.serviceName" placeholder="所属服务商名称" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="启用状态" prop="blOpen">
<el-radio-group v-model="form.blOpen">
<el-radio value="1">启用</el-radio>
<el-radio value="0">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否多票" prop="blMultiple">
<el-radio-group v-model="form.blMultiple">
<el-radio value="1">是</el-radio>
<el-radio value="0">否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目的港/目的地" prop="desPort">
<el-input v-model="form.desPort" placeholder="目的港或目的地区域" maxlength="500" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发货人名称" prop="shipperName">
<el-input v-model="form.shipperName" placeholder="请输入发货人名称" maxlength="255" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="货物类型" prop="cargoType">
<el-input v-model="form.cargoType" maxlength="255" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="付款方式" prop="payType">
<el-input v-model="form.payType" maxlength="255" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="末端服务商" prop="finalServiceName">
<el-input v-model="form.finalServiceName" maxlength="255" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="轨迹查询类型" prop="queryType">
<el-input v-model="form.queryType" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="轨迹编码" prop="trajectoryCode">
<el-input v-model="form.trajectoryCode" maxlength="60" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="重量类型" prop="weightType">
<el-input v-model="form.weightType" maxlength="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="抛货系数" prop="dimDivisor">
<el-input-number v-model="form.dimDivisor" :min="0" :precision="0" controls-position="right" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发货类型" prop="shipmentType">
<el-input v-model="form.shipmentType" maxlength="32" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="计重规则" prop="weightRule">
<el-input v-model="form.weightRule" type="textarea" :rows="2" maxlength="500" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="open = false">取 消</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="ServiceChannel">
import { nextTick } from 'vue'
import {
listServiceChannel, getServiceChannel, addServiceChannel,
updateServiceChannel, delServiceChannel, changeServiceChannelStatus
} from '@/api/masterdata/serviceChannel'
const { proxy } = getCurrentInstance()
const tableRef = ref()
const formRef = ref()
const open = ref(false)
const title = ref('')
const openOptions = [
{ label: '启用', value: '1' },
{ label: '停用', value: '0' }
]
const searchColumns = [
{ label: '渠道编码', prop: 'channelCode', component: 'input', placeholder: '渠道编码', width: '160px' },
{ label: '渠道名称', prop: 'channelName', component: 'input', placeholder: '模糊查询', width: '180px' },
{ label: '服务商编码', prop: 'serviceCode', component: 'input', placeholder: '服务商编码', width: '160px' },
{ label: '服务商名称', prop: 'serviceName', component: 'input', placeholder: '模糊查询', width: '180px' },
{ label: '目的港/目的地', prop: 'desPort', component: 'input', placeholder: '模糊查询', width: '180px' },
{ label: '发货人名称', prop: 'shipperName', component: 'input', placeholder: '模糊查询', width: '180px' },
{ label: '状态', prop: 'blOpen', component: 'select', options: openOptions, placeholder: '启用状态', width: '140px', props: { clearable: true } }
]
const columns = [
{ label: '渠道编码', prop: 'channelCode', minWidth: 140, showOverflowTooltip: true },
{ label: '渠道名称', prop: 'channelName', minWidth: 180, showOverflowTooltip: true },
{ label: '服务商编码', prop: 'serviceCode', minWidth: 120, showOverflowTooltip: true },
{ label: '服务商名称', prop: 'serviceName', minWidth: 160, showOverflowTooltip: true },
{ label: '目的港/目的地', prop: 'desPort', minWidth: 160, showOverflowTooltip: true },
{ label: '发货人', prop: 'shipperName', minWidth: 140, showOverflowTooltip: true },
{ label: '启用', prop: 'blOpen', width: 80, slot: 'blOpen' },
{ label: '多票', prop: 'blMultiple', width: 70, slot: 'blMultiple' },
{ label: '货物类型', prop: 'cargoType', minWidth: 120, showOverflowTooltip: true },
{ label: '末端服务商', prop: 'finalServiceName', minWidth: 140, showOverflowTooltip: true },
{ label: '抛货系数', prop: 'dimDivisor', width: 90 },
{ label: '同步时间', prop: 'syncTime', width: 170, parseTime: true },
{ label: '操作', prop: 'action', width: 150, fixed: 'right', slot: 'action', showInToolbar: false }
]
const data = reactive({
form: {},
rules: {
channelCode: [{ required: true, message: '渠道编码不能为空', trigger: 'blur' }],
channelName: [{ required: true, message: '渠道名称不能为空', trigger: 'blur' }],
blOpen: [{ required: true, message: '请选择启用状态', trigger: 'change' }]
}
})
const { form, rules } = toRefs(data)
function queryList(params) {
return listServiceChannel(params)
}
function emptyForm() {
return {
id: undefined,
channelCode: '',
channelName: '',
serviceCode: '',
serviceName: '',
blOpen: '1',
blMultiple: '0',
desPort: '',
shipperName: '',
shipperId: '',
cargoType: '',
payType: '',
finalServiceName: '',
queryType: '',
trajectoryCode: '',
weightType: '',
dimDivisor: undefined,
shipmentType: '',
weightRule: '',
remarks: ''
}
}
function handleAdd() {
form.value = emptyForm()
title.value = '新增渠道'
open.value = true
nextTick(() => formRef.value && formRef.value.clearValidate())
}
function handleUpdate(row) {
getServiceChannel(row.id).then(res => {
form.value = Object.assign(emptyForm(), res.data || {})
title.value = '修改渠道'
open.value = true
nextTick(() => formRef.value && formRef.value.clearValidate())
})
}
function submitForm() {
formRef.value.validate(valid => {
if (!valid) return
const req = form.value.id ? updateServiceChannel(form.value) : addServiceChannel(form.value)
req.then(() => {
proxy.$modal.msgSuccess('保存成功')
open.value = false
tableRef.value?.refresh()
})
})
}
function handleDelete(ids) {
if (!ids || !ids.length) return
proxy.$modal.confirm('确认删除选中的 ' + ids.length + ' 条渠道?已被产品或供应商引用的渠道不能删除。').then(() => {
return delServiceChannel(ids.join(','))
}).then(() => {
proxy.$modal.msgSuccess('删除成功')
tableRef.value?.refresh()
}).catch(() => {})
}
function handleStatusChange(row) {
if (!row || !row.id || !row.channelCode) {
nextTick(() => {
row.blOpen = row.blOpen === '1' ? '0' : '1'
})
return
}
if (row.__originalOpen !== undefined && row.__originalOpen === row.blOpen) {
return
}
row.__originalOpen = row.blOpen === '1' ? '0' : '1'
const text = row.blOpen === '1' ? '启用' : '停用'
const name = row.channelName || row.channelCode
proxy.$modal.confirm('确认要' + text + '渠道「' + name + '」吗?').then(() => {
return changeServiceChannelStatus(row.id, row.blOpen)
}).then(() => {
row.__originalOpen = row.blOpen
proxy.$modal.msgSuccess(text + '成功')
}).catch(() => {
const restored = row.blOpen === '1' ? '0' : '1'
row.__originalOpen = restored
nextTick(() => { row.blOpen = restored })
})
}
function handleExport() {
proxy.download('masterdata/servicechannel/export', {
...tableRef.value.getQueryParams()
}, `service_channel_${new Date().getTime()}.xlsx`)
}
</script>
@@ -1,10 +1,7 @@
<!-- start catalog version: pick effective hour -->
<!-- start catalog version: pick effective hour;IMPORT_FRESH 对齐销售价规则 -->
<template>
<el-dialog v-model="visible" :title="dialogTitle" width="460px" append-to-body @closed="onClosed">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item :label="labNo">
<span>{{ form.versionNo || '-' }}</span>
</el-form-item>
<el-form-item :label="labEff" prop="effectiveTime">
<el-date-picker
v-model="form.effectiveTime"
@@ -17,6 +14,7 @@
:disabled-hours="disabledHours"
style="width: 100%"
@change="onTimeChange"
@visible-change="onPickerVisible"
/>
</el-form-item>
<div class="activate-hint">{{ hint }}</div>
@@ -28,6 +26,8 @@
</el-dialog>
</template>
<script setup>
import { isImportFreshBuildMode } from '@/views/price/set/utils/priceSet'
const props = defineProps({
loadVersions: { type: Function, required: true },
activateFn: { type: Function, required: true }
@@ -39,31 +39,43 @@ const submitting = ref(false)
const formRef = ref()
const versions = ref([])
const prevEff = ref()
const form = reactive({ psvId: undefined, versionNo: '', versionSeq: 0, effectiveTime: undefined })
const isImportFresh = ref(false)
const boundaryTimes = ref([])
const form = reactive({ psvId: undefined, versionNo: '', versionSeq: 0, effectiveTime: undefined, priceBuildMode: undefined })
const dialogTitle = '启动版本'
const labNo = '版本号'
const labEff = '生效时间'
const ph = '请选择生效时间'
const btnCancel = '取消'
const btnOk = '确认启动'
const beforePrev = '生效时间不能早于上一版本的生效时间'
const hitBoundary = '生效时间不能与已有版本的生效时间或失效时间相同'
const hint = computed(() => {
if (isImportFresh.value) {
return '重新建价:生效时间可落在历史版本区间内;落在版本 B 区间时,B 失效时间改为本次启动时间,本版本失效时间取 B 原失效时间,并同步产品/明细。不可与本类型已有版本的生效时间或失效时间相同。'
}
const base = '若生效时间晚于当前,版本将进入待生效;到时自动生效并使上一版本失效。'
return prevEff.value ? (base + '不得早于上一版:' + prevEff.value + '。') : base
})
const rules = {
const rules = computed(() => ({
effectiveTime: [
{ required: true, message: ph, trigger: 'change' },
{
validator: (_r, v, cb) => {
if (!v || !prevEff.value) return cb()
if (toMs(v) < toMs(prevEff.value)) return cb(new Error(beforePrev))
if (!v) return cb()
if (isImportFresh.value) {
const ms = toMs(v)
if (boundaryTimes.value.some(b => toMs(b) === ms)) {
return cb(new Error(hitBoundary))
}
return cb()
}
if (prevEff.value && toMs(v) < toMs(prevEff.value)) return cb(new Error(beforePrev))
cb()
},
trigger: 'change'
}
]
}
}))
function pad2(n) { return String(n).padStart(2, '0') }
function formatHour(d) {
if (!d || Number.isNaN(d.getTime())) return undefined
@@ -93,14 +105,29 @@ function findPrev(row) {
}
return prev ? normalizeToHour(prev.effectiveTime) : undefined
}
function pickDefault(prev) {
function collectBoundaryTimes(excludePsvId) {
const out = []
const seen = new Set()
for (const v of versions.value || []) {
if (!v || v.psvId == null || String(v.psvId) === String(excludePsvId)) continue
;[v.effectiveTime, v.expireTime].forEach(t => {
const n = t ? normalizeToHour(t) : ''
if (!n || seen.has(n)) return
seen.add(n)
out.push(n)
})
}
return out
}
function pickDefault(prev, importFresh) {
const d = new Date()
d.setHours(9, 0, 0, 0)
const now = formatHour(d)
if (!prev) return now
if (importFresh || !prev) return now
return toMs(now) >= toMs(prev) ? now : prev
}
function disabledDate(date) {
if (isImportFresh.value) return false
if (!prevEff.value || !date) return false
const prevDay = new Date(String(prevEff.value).replace(/-/g, '/'))
prevDay.setHours(0, 0, 0, 0)
@@ -109,11 +136,25 @@ function disabledDate(date) {
return d.getTime() < prevDay.getTime()
}
function disabledHours() {
if (!prevEff.value) return []
const prevD = new Date(String(prevEff.value).replace(/-/g, '/'))
const curD = form.effectiveTime
? new Date(String(normalizeToHour(form.effectiveTime)).replace(/-/g, '/'))
: new Date()
if (isImportFresh.value) {
const hours = new Set()
for (const b of boundaryTimes.value) {
const bd = new Date(String(b).replace(/-/g, '/'))
if (
bd.getFullYear() === curD.getFullYear() &&
bd.getMonth() === curD.getMonth() &&
bd.getDate() === curD.getDate()
) {
hours.add(bd.getHours())
}
}
return [...hours]
}
if (!prevEff.value) return []
const prevD = new Date(String(prevEff.value).replace(/-/g, '/'))
if (prevD.getFullYear() !== curD.getFullYear() || prevD.getMonth() !== curD.getMonth() || prevD.getDate() !== curD.getDate()) return []
const hours = []
for (let h = 0; h < prevD.getHours(); h++) hours.push(h)
@@ -123,6 +164,9 @@ function onTimeChange(val) {
form.effectiveTime = normalizeToHour(val)
formRef.value && formRef.value.validateField('effectiveTime')
}
function onPickerVisible(visible) {
if (visible && formRef.value) formRef.value.validateField('effectiveTime')
}
async function open(row) {
if (!row || row.psvId == null) return
const list = await props.loadVersions()
@@ -131,15 +175,21 @@ async function open(row) {
form.psvId = found.psvId
form.versionNo = found.versionNo || ''
form.versionSeq = found.versionSeq
form.priceBuildMode = found.priceBuildMode
isImportFresh.value = isImportFreshBuildMode(found.priceBuildMode)
prevEff.value = findPrev(found)
form.effectiveTime = pickDefault(prevEff.value)
boundaryTimes.value = isImportFresh.value ? collectBoundaryTimes(found.psvId) : []
form.effectiveTime = pickDefault(prevEff.value, isImportFresh.value)
visible.value = true
}
function onClosed() {
form.psvId = undefined
form.versionNo = ''
form.effectiveTime = undefined
form.priceBuildMode = undefined
prevEff.value = undefined
isImportFresh.value = false
boundaryTimes.value = []
formRef.value && formRef.value.clearValidate()
}
function submit() {
@@ -29,8 +29,8 @@
<div class="build-choice__cards">
<button
type="button"
class="build-card build-card--pull"
:class="{ selected: buildMode === 'COPY' }"
class="build-card build-card--import"
:class="{ selected: buildMode === 'IMPORT' }"
:disabled="!!psvId || choosing || finishing"
@click="chooseMode(true)"
>
@@ -39,8 +39,8 @@
</button>
<button
type="button"
class="build-card build-card--import"
:class="{ selected: buildMode === 'IMPORT' }"
class="build-card build-card--import-fresh"
:class="{ selected: buildMode === 'IMPORT_FRESH' }"
:disabled="!!psvId || choosing || finishing"
@click="chooseMode(false)"
>
@@ -183,7 +183,7 @@ const route = useRoute()
const labTip = '创建价格方式'
const labModeCopy = '按现有生效版本建价'
const labModeCopyDesc = '先本地预览已生效版本产品,点击「完成」时再复制到待生效草稿'
const labModeFresh = '重新建产品价'
const labModeFresh = '新建产品价'
const labModeFreshDesc = "不沿用已生效版本;导入解析后先展示,点「完成」时再写入草稿"
const labImport = '导入 Excel 建价'
const labImportAll = '全部导入(多国家价表)'
@@ -300,9 +300,10 @@ function applyVersion(ver) {
}
psvId.value = ver.psvId
versionNo.value = ver.versionNo || ''
// Catalog: IMPORT = 沿用生效版; IMPORT_FRESH = 重新建价. Legacy COPY → IMPORT.
const mode = String(ver.priceBuildMode || '').toUpperCase()
if (mode === 'COPY') buildMode.value = 'COPY'
else if (mode === 'IMPORT' || mode === 'PULL') buildMode.value = 'IMPORT'
if (mode === 'IMPORT' || mode === 'COPY') buildMode.value = 'IMPORT'
else if (mode === 'IMPORT_FRESH' || mode === 'PULL') buildMode.value = 'IMPORT_FRESH'
}
async function loadProducts() {
@@ -329,7 +330,7 @@ async function bootstrap() {
psvId.value = Number(qPsv)
versionNo.value = (route.query && route.query.versionNo) ? String(route.query.versionNo) : ''
editingExisting.value = true
if (!buildMode.value) buildMode.value = 'IMPORT'
if (!buildMode.value) buildMode.value = 'IMPORT_FRESH'
await loadProducts()
}
} finally {
@@ -337,10 +338,10 @@ async function bootstrap() {
}
}
/** Mode select: local only, no DB write. */
/** Mode select: local only, no DB write. inherit=IMPORT, fresh=IMPORT_FRESH. */
async function chooseMode(copyFromLatest) {
if (psvId.value || choosing.value || finishing.value) return
const next = copyFromLatest ? 'COPY' : 'IMPORT'
const next = copyFromLatest ? 'IMPORT' : 'IMPORT_FRESH'
if (buildMode.value === next && (copyFromLatest ? rows.value.length : true)) {
if (copyFromLatest && rows.value.length) return
if (!copyFromLatest && !rows.value.length && !pendingImports.value.length) return
@@ -426,7 +427,7 @@ async function persistDraft() {
proxy.$modal.msgWarning('请先选择建价方式')
return false
}
const res = await props.prepareFn(buildMode.value === 'COPY')
const res = await props.prepareFn(buildMode.value === 'IMPORT')
applyVersion(res.data)
return !!psvId.value
}
@@ -682,8 +683,10 @@ onMounted(() => { bootstrap() })
.build-card:hover:not(:disabled) { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06); }
.build-card:disabled { opacity: 0.55; cursor: not-allowed; }
.build-card__title { font-size: 16px; font-weight: 600; color: #303133; margin-bottom: 10px; }
.build-card--pull .build-card__title { color: #67c23a; }
.build-card--import .build-card__title { color: #409eff; }
.build-card--import-fresh .build-card__title { color: #e6a23c; }
.build-card--import:hover:not(:disabled) { border-color: #409eff; }
.build-card--import-fresh:hover:not(:disabled) { border-color: #e6a23c; }
.build-card__desc { font-size: 13px; line-height: 1.6; color: #909399; }
.filters { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.filters__left { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
+6 -5
View File
@@ -36,6 +36,7 @@
placeholder="客户名称/客户代码/对外代码"
multiple
remote-search
option-select
/>
</template>
<template #search-productCodes="{ model }">
@@ -124,7 +125,7 @@
@click="openBatchFee"
>批量增加费用</el-button>
</el-col>
<!-- <el-col :span="1.5">
<el-col :span="1.5">
<el-button
type="primary"
plain
@@ -132,7 +133,7 @@
v-hasPermi="['price:billing:chargeWeightEdit']"
@click="openChargeWeightImport"
>批量修改计费重</el-button>
</el-col> -->
</el-col>
<el-col :span="1.5">
<el-button
type="info"
@@ -163,14 +164,14 @@
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
<!-- <el-button
<el-button
link
type="primary"
icon="Edit"
:disabled="row.refundStatus === 'REFUNDED'"
v-hasPermi="['price:billing:chargeWeightEdit']"
@click="openEditChargeWeight(row)"
>改计费重</el-button> -->
>改计费重</el-button>
<el-button
link
type="danger"
@@ -460,7 +461,7 @@ const columns = [
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
{ label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
{ label: '操作', prop: 'action', align: 'center', width: 150, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
{ label: '操作', prop: 'action', align: 'center', width: 240, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
]
const arrearsStatusOptions = [
@@ -81,6 +81,7 @@
placeholder="客户名称/客户代码/对外代码"
multiple
remote-search
option-select
/>
</template>
<template #search-orderStatuses="{ model }">
@@ -244,7 +245,7 @@ const columns = [
{ label: '订单状态', prop: 'orderStatus', align: 'center', width: 110, slot: 'orderStatus' },
{ label: '计费状态', prop: 'billingFailStatus', align: 'center', width: 140, slot: 'billingFailStatus' },
{ label: '计费时间', prop: 'bizDate', align: 'center', width: 170, parseTime: true },
{ label: '下单时间', prop: 'orderTime', align: 'center', width: 170, parseTime: true },
// { label: '下单时间', prop: 'orderTime', align: 'center', width: 170, parseTime: true },
{ label: '提货时间', prop: 'pickupDate', align: 'center', width: 170, parseTime: true },
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
@@ -75,7 +75,7 @@
:disabled="!canChooseBuildMode || !isFixedType(setType)"
@click="chooseBuildMode('IMPORT_FRESH')"
>
<div class="build-card__title">Excel 重新建价</div>
<div class="build-card__title">Excel 新建价</div>
<div class="build-card__desc">不延用以前的产品价格,不带出上一版,上传 Excel 直接生成套餐销售价</div>
</button>
</div>
@@ -227,7 +227,7 @@ import {
surchargeAppliesToRow,
toDetailPayload
} from '../../utils/priceSet'
import { getPriceSet, listSetDetailByProduct, importSetDetailPricesAsync, pullSetDetailsFromLibraryAsync, previewPullFromLibrary, previewImportDetailPrices, updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set'
import { getPriceSet, listSetDetailByProduct, listSetDetailByVersion, importSetDetailPricesAsync, pullSetDetailsFromLibraryAsync, previewPullFromLibrary, previewImportDetailPrices, updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set'
import { listRegionOptions } from '@/api/data/region'
const props = defineProps({
@@ -338,7 +338,7 @@ const importDesc = computed(() => {
return [
'适用威狮B2C小包价表(含「产品目录」)。',
'不延用以前的产品价格;导入按「产品+国家」写入套餐销售价,直接生成产品明细。',
'同一产品下重复国家+重量段将整单失败。仅固定价套餐「Excel 重新建价」可用。'
'同一产品下重复国家+重量段将整单失败。仅固定价套餐「Excel 新建价」可用。'
]
}
return [
@@ -351,7 +351,7 @@ const importDesc = computed(() => {
const typeHint = computed(() => {
if (isPullBuildMode(props.priceBuildMode)) return '· 按产品价建价'
if (isImportBuildMode(props.priceBuildMode)) return '· 按上一版 Excel 建价'
if (isImportFreshBuildMode(props.priceBuildMode)) return '· Excel 重新建价'
if (isImportFreshBuildMode(props.priceBuildMode)) return '· Excel 新建价'
if (hasBuildMode.value) return '· 当前:' + setTypeLabel(props.setType)
return '· 请选择创建价格方式'
})
@@ -437,12 +437,8 @@ async function loadLatestPriorVersionDraft() {
return row
})
if (!products.length) return false
const chunks = await Promise.all(products.map(p =>
listSetDetailByProduct(prior.psvId, p.productCode)
.then(r => r.data || [])
.catch(() => [])
))
applyLocalDraft(products, chunks.flat())
const detailRes = await listSetDetailByVersion(prior.psvId)
applyLocalDraft(products, detailRes.data || [])
return true
} finally {
pulling.value = false
@@ -123,9 +123,21 @@ function toEffMs(v) {
return Number.isNaN(t) ? NaN : t
}
/** Sort by effective time descending; drafts without time last. */
function sortByEffectiveTime(list) {
/** draft / pending first, then effect, then expired; within group by effective time desc. */
function statusRank(v) {
const s = String(v && v.status != null ? v.status : '')
if (s === '0') return 0
if (s === '3') return 1
if (s === '1') return 2
if (s === '2') return 3
return 9
}
function sortVersions(list) {
return (list || []).slice().sort((a, b) => {
const ra = statusRank(a)
const rb = statusRank(b)
if (ra !== rb) return ra - rb
const ta = toEffMs(a)
const tb = toEffMs(b)
const aNull = Number.isNaN(ta)
@@ -143,7 +155,7 @@ const filteredVersions = computed(() => {
const filtered = statusTab.value === 'all'
? list
: list.filter(v => String(v.status) === statusTab.value)
return sortByEffectiveTime(filtered)
return sortVersions(filtered)
})
function detailCount(row) {
+2 -2
View File
@@ -235,8 +235,8 @@ const activateEffHitBoundaryMsg = '生效时间不能与已有版本的生效时
const activateDialogHint = computed(() => {
if (activateIsImportFresh.value) {
const base = activateMode.value === 'reschedule'
? 'Excel 重新建价:生效时间可落在历史版本区间内,将自动截断该版本失效时间并同步产品/明细。'
: 'Excel 重新建价:生效时间可落在历史版本区间内;落在版本 B 区间时,B 失效时间改为本次启动时间,本版本失效时间取 B 原失效时间,并同步双方产品/明细。'
? 'Excel 新建价:生效时间可落在历史版本区间内,将自动截断该版本失效时间并同步产品/明细。'
: 'Excel 新建价:生效时间可落在历史版本区间内;落在版本 B 区间时,B 失效时间改为本次启动时间,本版本失效时间取 B 原失效时间,并同步双方产品/明细。'
return base + '不可与已有版本的生效时间或失效时间相同。'
}
const base = activateMode.value === 'reschedule'