修改公共配置字典
This commit is contained in:
+11
@@ -2,6 +2,7 @@ package com.ruoyi.web.controller.system;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -59,6 +60,16 @@ public class SysDictDataController extends BaseController
|
||||
util.exportExcel(response, list, "字典数据");
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量查询正常状态字典数据(按 dictType 分组)
|
||||
*/
|
||||
@GetMapping(value = "/all")
|
||||
public AjaxResult all()
|
||||
{
|
||||
Map<String, List<SysDictData>> data = dictTypeService.selectDictDataAllMap();
|
||||
return success(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询字典数据详细
|
||||
*/
|
||||
|
||||
@@ -25,9 +25,15 @@ public class BmsProduct extends BaseEntity
|
||||
private String taxMode;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
/** 0启用(按日期展示已生效/待生效) 1已失效 */
|
||||
private String status;
|
||||
private String sourceSheet;
|
||||
private Long tmsId;
|
||||
private Integer countryCount;
|
||||
private List<BmsProductCountry> countries;
|
||||
/**
|
||||
* 查询用:ACTIVE已生效 / PENDING待生效 / EXPIRED已失效
|
||||
* (非表字段,由生效日期+status 推导)
|
||||
*/
|
||||
private String effectStatus;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,13 @@ public class BmsProductCountry implements Serializable
|
||||
private String leadTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
/** null=open; closed to next import effective_date */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date expireDate;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updatedAt;
|
||||
/** query only: true = expire_date is null */
|
||||
private Boolean openOnly;
|
||||
}
|
||||
|
||||
+15
-2
@@ -21,9 +21,22 @@ public interface BmsProductCountryMapper
|
||||
|
||||
public int deleteByProductCodes(String[] productCodes);
|
||||
|
||||
/** Max open (expire_date is null) effective_date for product */
|
||||
public Date selectMaxOpenEffectiveDate(@Param("productCode") String productCode);
|
||||
|
||||
/** Close open versions: set expire_date = newEffectiveDate where effective_date < newEffectiveDate */
|
||||
public int expireOpenByProductCode(
|
||||
@Param("productCode") String productCode,
|
||||
@Param("newEffectiveDate") Date newEffectiveDate);
|
||||
|
||||
/** Replace same-day version only */
|
||||
public int deleteOpenByProductCodeAndEffectiveDate(
|
||||
@Param("productCode") String productCode,
|
||||
@Param("effectiveDate") Date effectiveDate);
|
||||
|
||||
/**
|
||||
* 按产品 + 国家 + 重量落入重量段 + 计数时间过滤,匹配一条默认价
|
||||
* 只取 effective_date <= bizDate 的已生效明细,多匹配时按 effective_date desc 取最新。
|
||||
* Match by product + country + weight + bizDate.
|
||||
* effective_date <= bizDate and (expire_date is null or expire_date > bizDate).
|
||||
*/
|
||||
public BmsProductCountry selectMatchByProductCountryWeight(
|
||||
@Param("productCode") String productCode,
|
||||
|
||||
+79
-10
@@ -185,26 +185,21 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
{
|
||||
product.setStatus("0");
|
||||
productMapper.insertBmsProduct(product);
|
||||
saveCountries(product.getProductCode(), countries);
|
||||
countryNum += saveCountriesVersioned(product.getProductCode(), countries);
|
||||
successNum++;
|
||||
countryNum += countries.size();
|
||||
}
|
||||
else if (updateSupport)
|
||||
{
|
||||
product.setProductId(existing.getProductId());
|
||||
productMapper.updateBmsProduct(product);
|
||||
countryMapper.deleteByProductCode(product.getProductCode());
|
||||
saveCountries(product.getProductCode(), countries);
|
||||
countryNum += saveCountriesVersioned(product.getProductCode(), countries);
|
||||
updateNum++;
|
||||
countryNum += countries.size();
|
||||
}
|
||||
else if (!countries.isEmpty())
|
||||
{
|
||||
// Product already exists and "update" unchecked: still refresh country/fee rows from Excel.
|
||||
countryMapper.deleteByProductCode(product.getProductCode());
|
||||
saveCountries(product.getProductCode(), countries);
|
||||
// Keep product master; version country rows by effective_date
|
||||
countryNum += saveCountriesVersioned(product.getProductCode(), countries);
|
||||
updateNum++;
|
||||
countryNum += countries.size();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -224,6 +219,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
String msg = "\u5bfc\u5165\u6210\u529f\uff1a\u65b0\u589e " + successNum + " \u4e2a\u4ea7\u54c1\uff0c\u66f4\u65b0 " + updateNum + " \u4e2a\uff0c\u8df3\u8fc7 "
|
||||
+ skipNum + " \u4e2a\uff1b\u5199\u5165\u56fd\u5bb6/\u91cd\u91cf\u6bb5\u660e\u7ec6 " + countryNum + " \u884c\u3002"
|
||||
+ "<br/>\u56fd\u5bb6\u660e\u7ec6\uff1a\u82e5\u65b0\u751f\u6548\u65e5\u665a\u4e8e\u4e0a\u6b21\uff0c\u4fdd\u7559\u5386\u53f2\u7248\u4e14\u4e0a\u6b21\u5931\u6548\u65e5=\u672c\u6b21\u751f\u6548\u65e5\uff1b\u540c\u65e5\u5219\u66ff\u6362\u5f53\u524d\u5f00\u653e\u7248\u3002"
|
||||
+ "<br/>\u8303\u56f4\uff1a\u4ea7\u54c1\u76ee\u5f55 + \u91cd\u91cf\u6bb5 + \u8fd0\u8d39/\u6302\u53f7\u8d39 + \u5c3a\u5bf8/\u5c3e\u7a0b/\u65f6\u6548\uff1b\u56fd\u5bb6\u7801\u6765\u81ea bms_region\u3002";
|
||||
if (!unmatchedCountries.isEmpty())
|
||||
{
|
||||
@@ -233,11 +229,47 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCountries(String productCode, List<BmsProductCountry> countries)
|
||||
/**
|
||||
* Versioned country save:
|
||||
* - newEff > last open eff: close old (expire_date = newEff), insert new (keep history)
|
||||
* - newEff equals last open: replace that open version only
|
||||
* - newEff < last open: reject
|
||||
*/
|
||||
private int saveCountriesVersioned(String productCode, List<BmsProductCountry> countries)
|
||||
{
|
||||
if (countries == null || countries.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
Date newEff = resolveImportEffectiveDate(countries);
|
||||
Date lastOpenEff = countryMapper.selectMaxOpenEffectiveDate(productCode);
|
||||
|
||||
if (lastOpenEff != null && newEff != null && newEff.before(lastOpenEff))
|
||||
{
|
||||
throw new ServiceException("\u751f\u6548\u65e5\u671f "
|
||||
+ formatDate(newEff) + " \u65e9\u4e8e\u5f53\u524d\u5f00\u653e\u7248 "
|
||||
+ formatDate(lastOpenEff) + "\uff0c\u8bf7\u4f7f\u7528\u66f4\u665a\u7684\u751f\u6548\u65e5\u671f\u5bfc\u5165");
|
||||
}
|
||||
|
||||
if (lastOpenEff != null && newEff != null && newEff.after(lastOpenEff))
|
||||
{
|
||||
// Previous open version expire_date = this effective_date; keep old rows
|
||||
countryMapper.expireOpenByProductCode(productCode, newEff);
|
||||
}
|
||||
else if (lastOpenEff != null)
|
||||
{
|
||||
// Same effective day (or both null): replace current open version only
|
||||
countryMapper.deleteOpenByProductCodeAndEffectiveDate(productCode, newEff);
|
||||
}
|
||||
|
||||
for (BmsProductCountry c : countries)
|
||||
{
|
||||
c.setProductCode(productCode);
|
||||
c.setExpireDate(null);
|
||||
if (c.getEffectiveDate() == null)
|
||||
{
|
||||
c.setEffectiveDate(newEff);
|
||||
}
|
||||
try
|
||||
{
|
||||
countryMapper.insertBmsProductCountry(c);
|
||||
@@ -250,9 +282,46 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
throw new ServiceException(
|
||||
"\u8bf7\u5148\u6267\u884c SQL\uff1abms-back/sql/price/product_country_fee_alter.sql\uff08\u7f3a\u5c11\u8fd0\u8d39/\u6302\u53f7\u8d39\u5b57\u6bb5\uff09");
|
||||
}
|
||||
if (tip.contains("expire_date") || tip.contains("uk_product_country_weight"))
|
||||
{
|
||||
throw new ServiceException(
|
||||
"\u8bf7\u5148\u6267\u884c SQL\uff1abms-back/sql/price/product_country_expire_alter.sql\uff08\u7f3a\u5c11\u5931\u6548\u65e5\u671f/\u7248\u672c\u552f\u4e00\u952e\uff09");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return countries.size();
|
||||
}
|
||||
|
||||
private Date resolveImportEffectiveDate(List<BmsProductCountry> countries)
|
||||
{
|
||||
Date max = null;
|
||||
for (BmsProductCountry c : countries)
|
||||
{
|
||||
if (c.getEffectiveDate() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (max == null || c.getEffectiveDate().after(max))
|
||||
{
|
||||
max = c.getEffectiveDate();
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
private String formatDate(Date d)
|
||||
{
|
||||
if (d == null)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
return new SimpleDateFormat("yyyy-MM-dd").format(d);
|
||||
}
|
||||
|
||||
private void saveCountries(String productCode, List<BmsProductCountry> countries)
|
||||
{
|
||||
saveCountriesVersioned(productCode, countries);
|
||||
}
|
||||
|
||||
private List<BmsProduct> parseCatalog(Sheet sheet)
|
||||
|
||||
+4
@@ -671,6 +671,10 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
for (BmsProductCountry row : rows)
|
||||
{
|
||||
if (row.getExpireDate() != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!matchDetailCountry(detail, row))
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.system.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.common.core.domain.entity.SysDictData;
|
||||
import com.ruoyi.common.core.domain.entity.SysDictType;
|
||||
|
||||
@@ -34,6 +35,13 @@ public interface ISysDictTypeService
|
||||
*/
|
||||
public List<SysDictData> selectDictDataByType(String dictType);
|
||||
|
||||
/**
|
||||
* 查询全部正常状态字典数据(按字典类型分组)
|
||||
*
|
||||
* @return key=dictType, value=字典数据列表
|
||||
*/
|
||||
public Map<String, List<SysDictData>> selectDictDataAllMap();
|
||||
|
||||
/**
|
||||
* 根据字典类型ID查询信息
|
||||
*
|
||||
|
||||
+23
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.system.service.impl;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -87,6 +88,28 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部正常状态字典数据(按字典类型分组)
|
||||
*/
|
||||
@Override
|
||||
public Map<String, List<SysDictData>> selectDictDataAllMap()
|
||||
{
|
||||
SysDictData query = new SysDictData();
|
||||
query.setStatus("0");
|
||||
Map<String, List<SysDictData>> dictDataMap = dictDataMapper.selectDictDataList(query).stream()
|
||||
.collect(Collectors.groupingBy(SysDictData::getDictType));
|
||||
Map<String, List<SysDictData>> result = new HashMap<>();
|
||||
for (Map.Entry<String, List<SysDictData>> entry : dictDataMap.entrySet())
|
||||
{
|
||||
List<SysDictData> sorted = entry.getValue().stream()
|
||||
.sorted(Comparator.comparing(SysDictData::getDictSort))
|
||||
.collect(Collectors.toList());
|
||||
result.put(entry.getKey(), sorted);
|
||||
DictUtils.setDictCache(entry.getKey(), sorted);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典类型ID查询信息
|
||||
*
|
||||
|
||||
@@ -17,15 +17,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="lastMile" column="last_mile" />
|
||||
<result property="leadTime" column="lead_time" />
|
||||
<result property="effectiveDate" column="effective_date" />
|
||||
<result property="expireDate" column="expire_date" />
|
||||
<result property="createdAt" column="created_at" />
|
||||
<result property="updatedAt" column="updated_at" />
|
||||
</resultMap>
|
||||
|
||||
<select id="selectBmsProductCountryList" parameterType="BmsProductCountry" resultMap="BmsProductCountryResult">
|
||||
<sql id="selectVo">
|
||||
select detail_id, product_code, country_name, country_code, weight_min, weight_max,
|
||||
freight_price, registration_fee, size_limit, last_mile, lead_time,
|
||||
effective_date, created_at, updated_at
|
||||
effective_date, expire_date, created_at, updated_at
|
||||
from bms_product_country
|
||||
</sql>
|
||||
|
||||
<select id="selectBmsProductCountryList" parameterType="BmsProductCountry" resultMap="BmsProductCountryResult">
|
||||
<include refid="selectVo"/>
|
||||
<where>
|
||||
<if test="productCode != null and productCode != ''">
|
||||
AND product_code = #{productCode}
|
||||
@@ -42,19 +47,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="weightMax != null">
|
||||
AND weight_max = #{weightMax}
|
||||
</if>
|
||||
<if test="effectiveDate != null">
|
||||
AND effective_date = #{effectiveDate}
|
||||
</if>
|
||||
<if test="openOnly != null and openOnly">
|
||||
AND expire_date is null
|
||||
</if>
|
||||
</where>
|
||||
order by country_name asc, weight_min asc, detail_id asc
|
||||
order by effective_date desc, country_name asc, weight_min asc, detail_id asc
|
||||
</select>
|
||||
|
||||
<insert id="insertBmsProductCountry" parameterType="BmsProductCountry" useGeneratedKeys="true" keyProperty="detailId">
|
||||
insert into bms_product_country (
|
||||
product_code, country_name, country_code, weight_min, weight_max,
|
||||
freight_price, registration_fee, size_limit, last_mile, lead_time,
|
||||
effective_date, created_at
|
||||
effective_date, expire_date, created_at
|
||||
) values (
|
||||
#{productCode}, #{countryName}, #{countryCode}, #{weightMin}, #{weightMax},
|
||||
#{freightPrice}, #{registrationFee}, #{sizeLimit}, #{lastMile}, #{leadTime},
|
||||
#{effectiveDate}, sysdate()
|
||||
#{effectiveDate}, #{expireDate}, sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
@@ -69,11 +80,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<select id="selectMaxOpenEffectiveDate" resultType="java.util.Date">
|
||||
select max(effective_date) from bms_product_country
|
||||
where product_code = #{productCode}
|
||||
and expire_date is null
|
||||
</select>
|
||||
|
||||
<update id="expireOpenByProductCode">
|
||||
update bms_product_country
|
||||
set expire_date = #{newEffectiveDate},
|
||||
updated_at = sysdate()
|
||||
where product_code = #{productCode}
|
||||
and expire_date is null
|
||||
and (effective_date is null or effective_date < #{newEffectiveDate})
|
||||
</update>
|
||||
|
||||
<delete id="deleteOpenByProductCodeAndEffectiveDate">
|
||||
delete from bms_product_country
|
||||
where product_code = #{productCode}
|
||||
and expire_date is null
|
||||
and (
|
||||
(#{effectiveDate} is null and effective_date is null)
|
||||
or effective_date = #{effectiveDate}
|
||||
)
|
||||
</delete>
|
||||
|
||||
<select id="selectMatchByProductCountryWeight" resultMap="BmsProductCountryResult">
|
||||
select detail_id, product_code, country_name, country_code, weight_min, weight_max,
|
||||
freight_price, registration_fee, size_limit, last_mile, lead_time,
|
||||
effective_date, created_at, updated_at
|
||||
from bms_product_country
|
||||
<include refid="selectVo"/>
|
||||
where product_code = #{productCode}
|
||||
and weight_min <= #{weight}
|
||||
and weight_max >= #{weight}
|
||||
@@ -89,7 +122,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</otherwise>
|
||||
</choose>
|
||||
<if test="bizDate != null">
|
||||
and effective_date <= #{bizDate}
|
||||
and (effective_date is null or effective_date <= #{bizDate})
|
||||
and (expire_date is null or expire_date > #{bizDate})
|
||||
</if>
|
||||
<if test="bizDate == null">
|
||||
and expire_date is null
|
||||
</if>
|
||||
order by effective_date desc, weight_min asc, detail_id asc
|
||||
limit 1
|
||||
|
||||
@@ -58,6 +58,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="status != null and status != ''">
|
||||
AND p.status = #{status}
|
||||
</if>
|
||||
<if test="effectStatus != null and effectStatus != ''">
|
||||
<choose>
|
||||
<when test="effectStatus == 'EXPIRED'">
|
||||
AND p.status = '1'
|
||||
</when>
|
||||
<when test="effectStatus == 'PENDING'">
|
||||
AND p.status = '0'
|
||||
AND (p.effective_date is null or p.effective_date > curdate())
|
||||
</when>
|
||||
<when test="effectStatus == 'ACTIVE'">
|
||||
AND p.status = '0'
|
||||
AND p.effective_date is not null
|
||||
AND p.effective_date <= curdate()
|
||||
</when>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="cargoAttr != null and cargoAttr != ''">
|
||||
AND p.cargo_attr like concat('%', #{cargoAttr}, '%')
|
||||
</if>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- BMS product effect status dict
|
||||
-- dict_type: bms_product_effect_status
|
||||
-- values: ACTIVE / PENDING / EXPIRED
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
|
||||
SELECT N'产品生效状态', 'bms_product_effect_status', '0', 'admin', sysdate(), N'产品列表:已生效/待生效/已失效'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bms_product_effect_status');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 1, N'已生效', 'ACTIVE', 'bms_product_effect_status', '', 'success', 'N', '0', 'admin', sysdate(), N'启用且生效日期<=今天'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = 'ACTIVE');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 2, N'待生效', 'PENDING', 'bms_product_effect_status', '', 'warning', 'N', '0', 'admin', sysdate(), N'启用且生效日期为空或>今天'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = 'PENDING');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 3, N'已失效', 'EXPIRED', 'bms_product_effect_status', '', 'info', 'N', '0', 'admin', sysdate(), N'手动标记失效 status=1'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = 'EXPIRED');
|
||||
@@ -0,0 +1,16 @@
|
||||
-- BMS product tax mode dict
|
||||
-- dict_type: bms_tax_mode
|
||||
-- values: TAX_INCLUDED / TAX_EXCLUDED
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
|
||||
SELECT N'税模式', 'bms_tax_mode', '0', 'admin', sysdate(), N'产品列表:包税/不包税'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bms_tax_mode');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 1, N'包税', 'TAX_INCLUDED', 'bms_tax_mode', '', 'success', 'Y', '0', 'admin', sysdate(), N''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_tax_mode' AND dict_value = 'TAX_INCLUDED');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 2, N'不包税', 'TAX_EXCLUDED', 'bms_tax_mode', '', 'warning', 'N', '0', 'admin', sysdate(), N''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_tax_mode' AND dict_value = 'TAX_EXCLUDED');
|
||||
@@ -39,12 +39,14 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
|
||||
last_mile varchar(500) DEFAULT NULL COMMENT '尾程派送说明',
|
||||
lead_time varchar(100) DEFAULT NULL COMMENT '参考时效',
|
||||
effective_date date DEFAULT NULL COMMENT '产品价表生效日期',
|
||||
expire_date date DEFAULT NULL COMMENT '失效日期(空=长期;新导入生效日=上一版失效日)',
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (detail_id),
|
||||
UNIQUE KEY uk_product_country_weight (product_code, country_name, weight_min, weight_max),
|
||||
UNIQUE KEY uk_product_country_weight_eff (product_code, country_name, weight_min, weight_max, effective_date),
|
||||
KEY idx_country_code (country_code),
|
||||
KEY idx_weight_match (product_code, country_code, weight_min, weight_max),
|
||||
KEY idx_product_eff (product_code, effective_date, expire_date),
|
||||
CONSTRAINT fk_product_country_code FOREIGN KEY (product_code) REFERENCES bms_product (product_code) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品国家明细-重量段+运费挂号费+尺寸/尾程/时效';
|
||||
|
||||
@@ -54,6 +56,9 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
|
||||
-- ALTER TABLE bms_product_country DROP INDEX uk_product_country;
|
||||
-- ALTER TABLE bms_product_country ADD UNIQUE KEY uk_product_country_weight (product_code, country_name, weight_min, weight_max);
|
||||
-- ALTER TABLE bms_product_country ADD KEY idx_weight_match (product_code, country_code, weight_min, weight_max);
|
||||
-- ALTER TABLE bms_product_country ADD COLUMN expire_date date DEFAULT NULL COMMENT '失效日期' AFTER effective_date;
|
||||
-- ALTER TABLE bms_product_country DROP INDEX uk_product_country_weight;
|
||||
-- ALTER TABLE bms_product_country ADD UNIQUE KEY uk_product_country_weight_eff (product_code, country_name, weight_min, weight_max, effective_date);
|
||||
|
||||
-- 菜单:挂在价格基础 2100 下(order_num=0 置顶)
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Add expire_date for product country versioning on re-import
|
||||
-- When new effective_date is later than previous, keep old rows and set their expire_date = new effective_date
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE bms_product_country
|
||||
ADD COLUMN expire_date date DEFAULT NULL COMMENT 'expire date (null=open; set to next import effective_date)' AFTER effective_date;
|
||||
|
||||
-- Allow multiple versions: unique includes effective_date
|
||||
ALTER TABLE bms_product_country DROP INDEX uk_product_country_weight;
|
||||
ALTER TABLE bms_product_country
|
||||
ADD UNIQUE KEY uk_product_country_weight_eff (product_code, country_name, weight_min, weight_max, effective_date);
|
||||
|
||||
ALTER TABLE bms_product_country
|
||||
ADD KEY idx_product_eff (product_code, effective_date, expire_date);
|
||||
@@ -25,6 +25,14 @@ export function getDicts(dictType) {
|
||||
})
|
||||
}
|
||||
|
||||
// 全量查询字典(按 dictType 分组)
|
||||
export function getDictAll() {
|
||||
return request({
|
||||
url: '/system/dict/data/all',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增字典数据
|
||||
export function addData(data) {
|
||||
return request({
|
||||
|
||||
@@ -9,6 +9,7 @@ import useUserStore from '@/store/modules/user'
|
||||
import useLockStore from '@/store/modules/lock'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import usePermissionStore from '@/store/modules/permission'
|
||||
import useDictStore from '@/store/modules/dict'
|
||||
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
@@ -43,6 +44,8 @@ router.beforeEach(async (to, from) => {
|
||||
try {
|
||||
// 拉取user_info信息
|
||||
await useUserStore().getInfo()
|
||||
// 全量字典单独接口,按 dictType code 本地匹配
|
||||
await useDictStore().loadAllDicts()
|
||||
isRelogin.show = false
|
||||
// 根据roles权限生成可访问的路由
|
||||
const accessRoutes = await usePermissionStore().generateRoutes()
|
||||
@@ -57,6 +60,7 @@ router.beforeEach(async (to, from) => {
|
||||
return { ...to, replace: true }
|
||||
} catch (err) {
|
||||
await useUserStore().logOut()
|
||||
useDictStore().cleanDict()
|
||||
ElMessage.error(err)
|
||||
return { path: '/' }
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { getDictAll } from '@/api/system/dict/data'
|
||||
|
||||
const useDictStore = defineStore(
|
||||
'dict',
|
||||
{
|
||||
state: () => ({
|
||||
dict: new Array()
|
||||
dict: new Array(),
|
||||
loaded: false
|
||||
}),
|
||||
actions: {
|
||||
// 获取字典
|
||||
getDict(_key) {
|
||||
if (_key == null && _key == "") {
|
||||
if (_key == null || _key === '') {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
@@ -19,19 +22,49 @@ const useDictStore = defineStore(
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
},
|
||||
// 设置字典
|
||||
// 设置字典(同 key 覆盖)
|
||||
setDict(_key, value) {
|
||||
if (_key !== null && _key !== "") {
|
||||
if (_key !== null && _key !== '') {
|
||||
for (let i = 0; i < this.dict.length; i++) {
|
||||
if (this.dict[i].key == _key) {
|
||||
this.dict[i].value = value
|
||||
return
|
||||
}
|
||||
}
|
||||
this.dict.push({
|
||||
key: _key,
|
||||
value: value
|
||||
})
|
||||
}
|
||||
},
|
||||
// 全量写入:{ dictType: [{dictLabel,dictValue,...}] }
|
||||
setDictMap(dictMap) {
|
||||
this.cleanDict()
|
||||
if (!dictMap || typeof dictMap !== 'object') {
|
||||
return
|
||||
}
|
||||
Object.keys(dictMap).forEach(dictType => {
|
||||
const list = dictMap[dictType] || []
|
||||
this.setDict(dictType, list.map(p => ({
|
||||
label: p.dictLabel,
|
||||
value: p.dictValue,
|
||||
elTagType: p.listClass,
|
||||
elTagClass: p.cssClass
|
||||
})))
|
||||
})
|
||||
this.loaded = true
|
||||
},
|
||||
// 登录后单独拉取全量字典
|
||||
loadAllDicts() {
|
||||
return getDictAll().then(res => {
|
||||
this.setDictMap(res.data || {})
|
||||
return res.data
|
||||
})
|
||||
},
|
||||
// 删除字典
|
||||
removeDict(_key) {
|
||||
var bln = false
|
||||
try {
|
||||
for (let i = 0; i < this.dict.length; i++) {
|
||||
if (this.dict[i].key == _key) {
|
||||
@@ -40,13 +73,14 @@ const useDictStore = defineStore(
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
bln = false
|
||||
return false
|
||||
}
|
||||
return bln
|
||||
return false
|
||||
},
|
||||
// 清空字典
|
||||
cleanDict() {
|
||||
this.dict = new Array()
|
||||
this.loaded = false
|
||||
},
|
||||
// 初始字典
|
||||
initDict() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { login, logout, getInfo } from '@/api/login'
|
||||
import { getToken, setToken, removeToken } from '@/utils/auth'
|
||||
import { isHttp, isEmpty } from "@/utils/validate"
|
||||
import useLockStore from '@/store/modules/lock'
|
||||
import useDictStore from '@/store/modules/dict'
|
||||
import defAva from '@/assets/images/profile.jpg'
|
||||
|
||||
const useUserStore = defineStore(
|
||||
@@ -82,6 +83,7 @@ const useUserStore = defineStore(
|
||||
this.token = ''
|
||||
this.roles = []
|
||||
this.permissions = []
|
||||
useDictStore().cleanDict()
|
||||
removeToken()
|
||||
resolve()
|
||||
}).catch(error => {
|
||||
|
||||
@@ -2,23 +2,29 @@ import useDictStore from '@/store/modules/dict'
|
||||
import { getDicts } from '@/api/system/dict/data'
|
||||
|
||||
/**
|
||||
* 获取字典数据
|
||||
* 获取字典数据(优先使用登录后全量缓存,按 dictType code 匹配)
|
||||
*/
|
||||
export function useDict(...args) {
|
||||
const res = ref({})
|
||||
return (() => {
|
||||
args.forEach((dictType, index) => {
|
||||
args.forEach((dictType) => {
|
||||
res.value[dictType] = []
|
||||
const dicts = useDictStore().getDict(dictType)
|
||||
if (dicts) {
|
||||
res.value[dictType] = dicts
|
||||
} else {
|
||||
// 缓存未命中时按类型补拉(如字典管理刚改完被 removeDict)
|
||||
getDicts(dictType).then(resp => {
|
||||
res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
|
||||
res.value[dictType] = (resp.data || []).map(p => ({
|
||||
label: p.dictLabel,
|
||||
value: p.dictValue,
|
||||
elTagType: p.listClass,
|
||||
elTagClass: p.cssClass
|
||||
}))
|
||||
useDictStore().setDict(dictType, res.value[dictType])
|
||||
})
|
||||
}
|
||||
})
|
||||
return toRefs(res.value)
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,10 +195,9 @@
|
||||
<script setup name="Region">
|
||||
import { listRegion, getRegion, updateRegion, delRegion, changeRegionStatus, syncRegion } from "@/api/data/region"
|
||||
import { getSyncLog, listSyncLog, delSyncLog } from "@/api/data/region-log"
|
||||
import { nextTick, onBeforeUnmount } from "vue"
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, reactive, toRefs, watch, getCurrentInstance } from "vue"
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const tableRef = ref()
|
||||
const open = ref(false)
|
||||
const title = ref("")
|
||||
|
||||
@@ -30,26 +30,18 @@
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<template #col-taxMode="{ row }">
|
||||
<el-tag :type="row.taxMode === 'TAX_INCLUDED' ? 'success' : 'warning'" size="small">
|
||||
{{ row.taxMode === 'TAX_INCLUDED' ? '包税' : '不包税' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #col-cargoAttr="{ row }">
|
||||
<el-tag type="primary" size="small" effect="plain">{{ row.cargoAttr || '-' }}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #col-status="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
<dict-tag :options="bms_product_effect_status" :value="resolveEffectStatus(row)" />
|
||||
</template>
|
||||
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="openCountries(row)" v-hasPermi="['price:product:query']">产品明细</el-button>
|
||||
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '生效中不可编辑' : ''" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
|
||||
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '生效中不可删除' : ''" link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '已生效不可编辑' : ''" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
|
||||
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '已生效不可删除' : ''" link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
@@ -88,8 +80,12 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="税模式" prop="taxMode">
|
||||
<el-select v-model="form.taxMode" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="包税" value="TAX_INCLUDED" />
|
||||
<el-option label="不包税" value="TAX_EXCLUDED" />
|
||||
<el-option
|
||||
v-for="dict in bms_tax_mode"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -103,8 +99,8 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio value="0">正常</el-radio>
|
||||
<el-radio value="1">停用</el-radio>
|
||||
<el-radio value="0">启用(按生效日期自动:待生效/已生效)</el-radio>
|
||||
<el-radio value="1">已失效</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -146,6 +142,10 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时效" prop="leadTime" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="生效日期" prop="effectiveDate" width="110" align="center" />
|
||||
<el-table-column label="失效日期" width="110" align="center">
|
||||
<template #default="{ row }">{{ row.expireDate || '长期' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
title="产品 Excel 导入"
|
||||
action="/price/product/importData"
|
||||
:default-update-support="true"
|
||||
update-support-label="是否更新已经存在的产品主数据(国家/运费明细每次导入都会覆盖)"
|
||||
update-support-label="是否更新已存在的产品主数据(国家明细:新生效日晚于上次则保留历史,上次失效日=本次生效日)"
|
||||
@success="() => tableRef.refresh()"
|
||||
/>
|
||||
</div>
|
||||
@@ -165,6 +165,7 @@ import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||||
import { listProduct, getProduct, addProduct, updateProduct, delProduct, listProductCountries } from '@/api/price/product'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_product_effect_status, bms_tax_mode } = useDict('bms_product_effect_status', 'bms_tax_mode')
|
||||
const tableRef = ref()
|
||||
const importRef = ref()
|
||||
const formRef = ref()
|
||||
@@ -190,34 +191,32 @@ const columns = [
|
||||
{ prop: 'serviceLevel', label: '时效等级', width: 90, align: 'center' },
|
||||
{ prop: 'cargoAttr', label: '走货属性', minWidth: 120, slot: 'cargoAttr' },
|
||||
{ prop: 'destRegion', label: '目的区域', width: 90, align: 'center' },
|
||||
{ prop: 'taxMode', label: '税模式', width: 90, align: 'center', slot: 'taxMode' },
|
||||
{ prop: 'taxMode', label: '税模式', width: 90, align: 'center', dict: 'bms_tax_mode' },
|
||||
{ prop: 'effectiveDate', label: '生效日期', width: 120, align: 'center' },
|
||||
{ prop: 'countryCount', label: '国家数', width: 80, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', slot: 'status' },
|
||||
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
|
||||
{ prop: 'updateNote', label: '更新内容', minWidth: 160, showOverflowTooltip: true },
|
||||
{ prop: 'action', label: '操作', width: 240, fixed: 'right', slot: 'action' }
|
||||
]
|
||||
|
||||
const searchColumns = [
|
||||
{ prop: 'productCode', label: '产品代码' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'productCode', label: '产品代码', component: 'input', placeholder: '产品代码' },
|
||||
{ prop: 'productName', label: '产品名称', component: 'input', placeholder: '产品名称' },
|
||||
{
|
||||
prop: 'taxMode',
|
||||
label: '税模式',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '包税', value: 'TAX_INCLUDED' },
|
||||
{ label: '不包税', value: 'TAX_EXCLUDED' }
|
||||
]
|
||||
component: 'select',
|
||||
dict: 'bms_tax_mode',
|
||||
placeholder: '税模式',
|
||||
clearable: true
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
prop: 'effectStatus',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '正常', value: '0' },
|
||||
{ label: '停用', value: '1' }
|
||||
]
|
||||
component: 'select',
|
||||
dict: 'bms_product_effect_status',
|
||||
placeholder: '请选择状态',
|
||||
clearable: true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -235,13 +234,21 @@ function queryList(params) {
|
||||
return listProduct(params)
|
||||
}
|
||||
|
||||
// 判断产品是否"生效中":status='0'(未停用)且 effectiveDate <= 今天
|
||||
function todayStr() {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/** ACTIVE已生效 / PENDING待生效 / EXPIRED已失效 */
|
||||
function resolveEffectStatus(row) {
|
||||
if (!row) return 'PENDING'
|
||||
if (row.status === '1') return 'EXPIRED'
|
||||
if (!row.effectiveDate) return 'PENDING'
|
||||
return String(row.effectiveDate).slice(0, 10) <= todayStr() ? 'ACTIVE' : 'PENDING'
|
||||
}
|
||||
|
||||
// 已生效不可编辑/删除
|
||||
function isProductActive(row) {
|
||||
if (!row) return false
|
||||
if (row.status !== '0') return false
|
||||
if (!row.effectiveDate) return false
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
return String(row.effectiveDate).slice(0, 10) <= today
|
||||
return resolveEffectStatus(row) === 'ACTIVE'
|
||||
}
|
||||
|
||||
function reset() {
|
||||
@@ -269,7 +276,7 @@ function handleAdd() {
|
||||
|
||||
function handleUpdate(row) {
|
||||
if (isProductActive(row)) {
|
||||
proxy.$modal.msgError('生效中的产品不可编辑')
|
||||
proxy.$modal.msgError('已生效的产品不可编辑')
|
||||
return
|
||||
}
|
||||
reset()
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="计数时间" prop="bizDate">
|
||||
<el-form-item label="计费时间" prop="bizDate">
|
||||
<el-date-picker
|
||||
v-model="form.bizDate"
|
||||
type="date"
|
||||
@@ -67,7 +67,6 @@
|
||||
placeholder="默认今天,用于筛选已生效的产品明细"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-hint">只匹配 effective_date ≤ 计数时间的产品明细;多条匹配时取最新</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="handleQuote">计算价格</el-button>
|
||||
@@ -171,7 +170,7 @@ function handleProductChange(productCode) {
|
||||
return
|
||||
}
|
||||
listProductCountries(productCode).then(res => {
|
||||
countryOptions.value = uniqueCountries(res.data || [])
|
||||
countryOptions.value = uniqueCountries((res.data || []).filter(item => !item.expireDate))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ function ensureCountries(productCode) {
|
||||
if (!productCode) return Promise.resolve([])
|
||||
if (countryCache[productCode]) return Promise.resolve(countryCache[productCode])
|
||||
return listProductCountries(productCode).then(res => {
|
||||
const rows = res.data || []
|
||||
const rows = (res.data || []).filter(item => !item.expireDate)
|
||||
countryCache[productCode] = rows
|
||||
return rows
|
||||
})
|
||||
|
||||
@@ -281,7 +281,7 @@ function ensureCountries(productCode) {
|
||||
if (!productCode) return Promise.resolve([])
|
||||
if (countryCache[productCode]) return Promise.resolve(countryCache[productCode])
|
||||
return listProductCountries(productCode).then(res => {
|
||||
const rows = res.data || []
|
||||
const rows = (res.data || []).filter(item => !item.expireDate)
|
||||
countryCache[productCode] = rows
|
||||
return rows
|
||||
})
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
import DictDataDrawer from './detail'
|
||||
import useDictStore from '@/store/modules/dict'
|
||||
import { listType, getType, delType, addType, updateType, refreshCache } from "@/api/system/dict/type"
|
||||
import { getDictAll } from "@/api/system/dict/data"
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { sys_normal_disable } = useDict("sys_normal_disable")
|
||||
@@ -248,8 +249,10 @@ function handleExport() {
|
||||
/** 刷新缓存按钮操作 */
|
||||
function handleRefreshCache() {
|
||||
refreshCache().then(() => {
|
||||
return getDictAll()
|
||||
}).then(res => {
|
||||
useDictStore().setDictMap(res.data)
|
||||
proxy.$modal.msgSuccess("刷新成功")
|
||||
useDictStore().cleanDict()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user