feat(dict): 新增字典注解删除标记字段支持并修复库存计算空指针异常

- 在Dict注解中新增deleteFlag字段用于指定删除标记字段名
- 修改DictAspect切面逻辑支持删除标记字段的过滤查询
- 更新ProductDetailAppMapper.xml中的关联查询条件排序
- 修复ProductDetailAppServiceImpl中partPercent为空时的空指针异常
- 为ReceiptPageDto中的字典字段添加删除标记过滤配置
- 新增药物统计管理门户页面提供各类统计报表入口
This commit is contained in:
2026-02-24 17:30:23 +08:00
parent 8b993d5ddd
commit ff41aa9c04
6 changed files with 306 additions and 105 deletions

View File

@@ -173,29 +173,47 @@ public class ProductDetailAppServiceImpl extends ServiceImpl<InventoryItemMapper
productDetailPageDto.setInventoryStatusEnum_enumText(
EnumUtils.getInfoByValue(PublicationStatus.class, productDetailPageDto.getInventoryStatusEnum()));
// 计算包装单位数量
BigDecimal[] results
= productDetailPageDto.getQuantity().divideAndRemainder(productDetailPageDto.getPartPercent());
// 整数
productDetailPageDto.setNumber(results[0]);
// 余数
productDetailPageDto.setRemainder(results[1]);
// 计算包装单位数量 - 防止partPercent为null导致的空指针异常
BigDecimal partPercent = productDetailPageDto.getPartPercent();
if (partPercent != null && partPercent.compareTo(BigDecimal.ZERO) != 0) {
BigDecimal[] results = productDetailPageDto.getQuantity().divideAndRemainder(partPercent);
// 整数
productDetailPageDto.setNumber(results[0]);
// 余数
productDetailPageDto.setRemainder(results[1]);
// 计算采购总额
if (productDetailPageDto.getPurchasePrice() != null) {
productDetailPageDto
.setTotalPurchasePrice(productDetailPageDto.getPurchasePrice().multiply(results[0])
.add(productDetailPageDto.getPurchasePrice()
.divide(productDetailPageDto.getPartPercent(), 6, RoundingMode.HALF_UP)
.multiply(results[1])));
}
// 计算采购总额
if (productDetailPageDto.getPurchasePrice() != null) {
productDetailPageDto
.setTotalPurchasePrice(productDetailPageDto.getPurchasePrice().multiply(results[0])
.add(productDetailPageDto.getPurchasePrice()
.divide(partPercent, 6, RoundingMode.HALF_UP)
.multiply(results[1])));
}
// 计算售价总额
if (productDetailPageDto.getSalePrice() != null) {
productDetailPageDto.setTotalSalePrice(productDetailPageDto.getSalePrice().multiply(results[0])
.add(productDetailPageDto.getSalePrice()
.divide(productDetailPageDto.getPartPercent(), 6, RoundingMode.HALF_UP)
.multiply(results[1])));
// 计算售价总额
if (productDetailPageDto.getSalePrice() != null) {
productDetailPageDto.setTotalSalePrice(productDetailPageDto.getSalePrice().multiply(results[0])
.add(productDetailPageDto.getSalePrice()
.divide(partPercent, 6, RoundingMode.HALF_UP)
.multiply(results[1])));
}
} else {
// 如果partPercent为空或为0则直接设置数量避免计算
productDetailPageDto.setNumber(productDetailPageDto.getQuantity());
productDetailPageDto.setRemainder(BigDecimal.ZERO);
// 计算采购总额
if (productDetailPageDto.getPurchasePrice() != null) {
productDetailPageDto.setTotalPurchasePrice(
productDetailPageDto.getPurchasePrice().multiply(productDetailPageDto.getQuantity()));
}
// 计算售价总额
if (productDetailPageDto.getSalePrice() != null) {
productDetailPageDto.setTotalSalePrice(
productDetailPageDto.getSalePrice().multiply(productDetailPageDto.getQuantity()));
}
}
}
}
@@ -552,29 +570,47 @@ public class ProductDetailAppServiceImpl extends ServiceImpl<InventoryItemMapper
productDetailPageDto.setInventoryStatusEnum_enumText(
EnumUtils.getInfoByValue(PublicationStatus.class, productDetailPageDto.getInventoryStatusEnum()));
// 计算包装单位数量
BigDecimal[] results
= productDetailPageDto.getQuantity().divideAndRemainder(productDetailPageDto.getPartPercent());
// 整数
productDetailPageDto.setNumber(results[0]);
// 余数
productDetailPageDto.setRemainder(results[1]);
// 计算包装单位数量 - 防止partPercent为null导致的空指针异常
BigDecimal partPercent = productDetailPageDto.getPartPercent();
if (partPercent != null && partPercent.compareTo(BigDecimal.ZERO) != 0) {
BigDecimal[] results = productDetailPageDto.getQuantity().divideAndRemainder(partPercent);
// 整数
productDetailPageDto.setNumber(results[0]);
// 余数
productDetailPageDto.setRemainder(results[1]);
// 计算采购总额
if (productDetailPageDto.getPurchasePrice() != null) {
productDetailPageDto
.setTotalPurchasePrice(productDetailPageDto.getPurchasePrice().multiply(results[0])
.add(productDetailPageDto.getPurchasePrice()
.divide(productDetailPageDto.getPartPercent(), 6, RoundingMode.HALF_UP)
.multiply(results[1])));
}
// 计算采购总额
if (productDetailPageDto.getPurchasePrice() != null) {
productDetailPageDto
.setTotalPurchasePrice(productDetailPageDto.getPurchasePrice().multiply(results[0])
.add(productDetailPageDto.getPurchasePrice()
.divide(partPercent, 6, RoundingMode.HALF_UP)
.multiply(results[1])));
}
// 计算售价总额
if (productDetailPageDto.getSalePrice() != null) {
productDetailPageDto.setTotalSalePrice(productDetailPageDto.getSalePrice().multiply(results[0])
.add(productDetailPageDto.getSalePrice()
.divide(productDetailPageDto.getPartPercent(), 6, RoundingMode.HALF_UP)
.multiply(results[1])));
// 计算售价总额
if (productDetailPageDto.getSalePrice() != null) {
productDetailPageDto.setTotalSalePrice(productDetailPageDto.getSalePrice().multiply(results[0])
.add(productDetailPageDto.getSalePrice()
.divide(partPercent, 6, RoundingMode.HALF_UP)
.multiply(results[1])));
}
} else {
// 如果partPercent为空或为0则直接设置数量避免计算
productDetailPageDto.setNumber(productDetailPageDto.getQuantity());
productDetailPageDto.setRemainder(BigDecimal.ZERO);
// 计算采购总额
if (productDetailPageDto.getPurchasePrice() != null) {
productDetailPageDto.setTotalPurchasePrice(
productDetailPageDto.getPurchasePrice().multiply(productDetailPageDto.getQuantity()));
}
// 计算售价总额
if (productDetailPageDto.getSalePrice() != null) {
productDetailPageDto.setTotalSalePrice(
productDetailPageDto.getSalePrice().multiply(productDetailPageDto.getQuantity()));
}
}
}
}

View File

@@ -53,7 +53,7 @@ public class ReceiptPageDto {
/**
* 供应商
*/
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_supplier")
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_supplier", deleteFlag = "delete_flag")
@JsonSerialize(using = ToStringSerializer.class)
private Long supplierId;
private String supplierId_dictText;
@@ -63,7 +63,7 @@ public class ReceiptPageDto {
/**
* 目的仓库
*/
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_location")
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_location", deleteFlag = "delete_flag")
@JsonSerialize(using = ToStringSerializer.class)
private Long purposeLocationId;
private String purposeLocationId_dictText;
@@ -73,7 +73,7 @@ public class ReceiptPageDto {
/**
* 目的仓位
*/
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_location")
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_location", deleteFlag = "delete_flag")
@JsonSerialize(using = ToStringSerializer.class)
private Long purposeLocationStoreId;
private String purposeLocationStoreId_dictText;
@@ -86,7 +86,7 @@ public class ReceiptPageDto {
/**
* 经手人
*/
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_practitioner")
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_practitioner", deleteFlag = "delete_flag")
@JsonSerialize(using = ToStringSerializer.class)
private Long practitionerId;
private String practitionerId_dictText;
@@ -96,7 +96,7 @@ public class ReceiptPageDto {
/**
* 审批人
*/
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_practitioner")
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_practitioner", deleteFlag = "delete_flag")
private Long approverId;
private String approverId_dictText;
@Excel(name = "审批人", sort = 10)
@@ -111,7 +111,7 @@ public class ReceiptPageDto {
/**
* 申请人
*/
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_practitioner")
@Dict(dictCode = "id", dictText = "name", dictTable = "adm_practitioner", deleteFlag = "delete_flag")
@JsonSerialize(using = ToStringSerializer.class)
private Long applicantId;
private String applicantId_dictText;

View File

@@ -82,23 +82,23 @@
LEFT JOIN adm_charge_item_definition acid
ON wii.item_id = acid.instance_id
AND acid.delete_flag = '0'
LEFT JOIN adm_charge_item_def_detail acidd
ON acid.id = acidd.definition_id
AND acidd.condition_value = wii.lot_number
AND acidd.delete_flag = '0'
LEFT JOIN adm_location al
ON wii.location_id = al.id
AND al.delete_flag = '0'
LEFT JOIN adm_location al2
ON wii.location_store_id = al2.id
AND al2.delete_flag = '0'
LEFT JOIN adm_supplier as2
ON wii.supplier_id = as2.id
AND as2.delete_flag = '0'
WHERE wii.item_table = #{medMedicationDefinition}
AND acidd.condition_code = #{lotNumberCost}
AND wii.delete_flag = '0'
ORDER BY wii.py_str)
LEFT JOIN adm_charge_item_def_detail acidd
ON acid.id = acidd.definition_id
AND acidd.condition_value = wii.lot_number
AND acidd.condition_code = #{lotNumberCost}
AND acidd.delete_flag = '0'
LEFT JOIN adm_location al
ON wii.location_id = al.id
AND al.delete_flag = '0'
LEFT JOIN adm_location al2
ON wii.location_store_id = al2.id
AND al2.delete_flag = '0'
LEFT JOIN adm_supplier as2
ON wii.supplier_id = as2.id
AND as2.delete_flag = '0'
WHERE wii.item_table = #{medMedicationDefinition}
AND wii.delete_flag = '0'
ORDER BY wii.py_str)
UNION
(SELECT wii.id AS inventory_id,
wii.production_date,
@@ -141,24 +141,23 @@
LEFT JOIN adm_charge_item_definition acid
ON wii.item_id = acid.instance_id
AND acid.delete_flag = '0'
LEFT JOIN adm_charge_item_def_detail acidd
ON acid.id = acidd.definition_id
AND acidd.condition_code = #{lotNumberCost}
AND acidd.condition_value = wii.lot_number
AND acidd.delete_flag = '0'
LEFT JOIN adm_location al
ON wii.location_id = al.id
AND al.delete_flag = '0'
LEFT JOIN adm_location al2
ON wii.location_store_id = al2.id
AND al2.delete_flag = '0'
LEFT JOIN adm_supplier as2
ON wii.supplier_id = as2.id
AND as2.delete_flag = '0'
WHERE wii.item_table = #{admDeviceDefinition}
AND acidd.condition_code = #{lotNumberCost}
AND wii.delete_flag = '0'
ORDER BY wii.py_str)
LEFT JOIN adm_charge_item_def_detail acidd
ON acid.id = acidd.definition_id
AND acidd.condition_code = #{lotNumberCost}
AND acidd.condition_value = wii.lot_number
AND acidd.delete_flag = '0'
LEFT JOIN adm_location al
ON wii.location_id = al.id
AND al.delete_flag = '0'
LEFT JOIN adm_location al2
ON wii.location_store_id = al2.id
AND al2.delete_flag = '0'
LEFT JOIN adm_supplier as2
ON wii.supplier_id = as2.id
AND as2.delete_flag = '0'
WHERE wii.item_table = #{admDeviceDefinition}
AND wii.delete_flag = '0'
ORDER BY wii.py_str)
) AS ii
${ew.customSqlSegment}
</select>
@@ -247,22 +246,22 @@
LEFT JOIN adm_charge_item_definition acid
ON wii.item_id = acid.instance_id
AND acid.delete_flag = '0'
LEFT JOIN adm_charge_item_def_detail acidd
ON acid.id = acidd.definition_id
AND acidd.condition_value = wii.lot_number
AND acidd.delete_flag = '0'
LEFT JOIN adm_location al
ON wii.location_id = al.id
AND al.delete_flag = '0'
LEFT JOIN adm_location al2
ON wii.location_store_id = al2.id
AND al2.delete_flag = '0'
LEFT JOIN adm_supplier as2
ON wii.supplier_id = as2.id
AND as2.delete_flag = '0'
WHERE wii.item_table = #{medMedicationDefinition}
AND acidd.condition_code = #{lotNumberCost}
AND wii.delete_flag = '0'
LEFT JOIN adm_charge_item_def_detail acidd
ON acid.id = acidd.definition_id
AND acidd.condition_code = #{lotNumberCost}
AND acidd.condition_value = wii.lot_number
AND acidd.delete_flag = '0'
LEFT JOIN adm_location al
ON wii.location_id = al.id
AND al.delete_flag = '0'
LEFT JOIN adm_location al2
ON wii.location_store_id = al2.id
AND al2.delete_flag = '0'
LEFT JOIN adm_supplier as2
ON wii.supplier_id = as2.id
AND as2.delete_flag = '0'
WHERE wii.item_table = #{medMedicationDefinition}
AND wii.delete_flag = '0'
UNION
SELECT wii.id AS inventory_id,
wii.production_date,
@@ -318,9 +317,8 @@
LEFT JOIN adm_supplier as2
ON wii.supplier_id = as2.id
AND as2.delete_flag = '0'
WHERE wii.item_table = #{admDeviceDefinition}
AND acidd.condition_code = #{lotNumberCost}
AND wii.delete_flag = '0') AS ii
WHERE wii.item_table = #{admDeviceDefinition}
AND wii.delete_flag = '0') AS ii
LEFT JOIN sys_dict_data sdd ON ii.unit_code = sdd.dict_value AND sdd.dict_type = 'unit_code'
LEFT JOIN sys_dict_data sdd2
ON ii.min_unit_code = sdd2.dict_value AND sdd2.dict_type = 'unit_code'

View File

@@ -11,4 +11,5 @@ public @interface Dict {
String dictCode(); // 字典类型字段
String dictText() default ""; // 回显字段,默认为空
String dictTable() default ""; // 表名,默认为空
String deleteFlag() default ""; // 删除标记字段名,默认为空(不过滤)
}

View File

@@ -95,8 +95,9 @@ public class DictAspect {
String dictCode = dictAnnotation.dictCode();
String dictText = dictAnnotation.dictText();
String dictTable = dictAnnotation.dictTable();
String deleteFlag = dictAnnotation.deleteFlag();
// 查询字典值
String dictLabel = queryDictLabel(dictTable, dictCode, dictText, fieldValue.toString());
String dictLabel = queryDictLabel(dictTable, dictCode, dictText, deleteFlag, fieldValue.toString());
if (dictLabel != null) {
try {
// 动态生成 _dictText 字段名
@@ -115,7 +116,7 @@ public class DictAspect {
}
}
private String queryDictLabel(String dictTable, String dictCode, String dictText, String dictValue) {
private String queryDictLabel(String dictTable, String dictCode, String dictText, String deleteFlag, String dictValue) {
if (!StringUtils.hasText(dictTable)) {
// 场景 1默认字典走DictUtils缓存dictTable 为空时)
return DictUtils.getDictLabel(dictCode, dictValue);
@@ -126,7 +127,15 @@ public class DictAspect {
// 如果 dictText 为空,回退到字典缓存查询
return DictUtils.getDictLabel(dictCode, dictValue);
}
String sql = String.format("SELECT %s FROM %s WHERE %s::varchar = ? LIMIT 1", dictText, dictTable, dictCode);
// 构建SQL支持 delete_flag 过滤
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append(String.format("SELECT %s FROM %s WHERE %s::varchar = ?", dictText, dictTable, dictCode));
// 如果指定了 deleteFlag 字段名,添加过滤条件
if (StringUtils.hasText(deleteFlag)) {
sqlBuilder.append(String.format(" AND %s = '0'", deleteFlag));
}
sqlBuilder.append(" LIMIT 1");
String sql = sqlBuilder.toString();
try {
return jdbcTemplate.queryForObject(sql, String.class, dictValue);
} catch (DataAccessException e) {

View File

@@ -0,0 +1,157 @@
<template>
<div class="app-container">
<div class="statistics-portal">
<h2 class="portal-title">药物统计管理门户</h2>
<p class="portal-description">选择您想要查看的统计报告类型</p>
<div class="statistics-grid">
<!-- 库存统计 -->
<el-card class="statistic-card" @click="goToPage('/medicationmanagement/statisticalManagement')">
<div class="card-content">
<div class="card-icon">
<i class="el-icon-data-analysis" style="font-size: 36px; color: #409EFF;"></i>
</div>
<div class="card-text">
<h3>库存统计</h3>
<p>查看药品库存情况</p>
</div>
</div>
</el-card>
<!-- 效期预警 -->
<el-card class="statistic-card" @click="goToPage('/medicationmanagement/statisticalManagement/earlyWarning')">
<div class="card-content">
<div class="card-icon">
<i class="el-icon-warning-outline" style="font-size: 36px; color: #E6A23C;"></i>
</div>
<div class="card-text">
<h3>效期预警</h3>
<p>查看即将过期的药品</p>
</div>
</div>
</el-card>
<!-- 药品使用情况 -->
<el-card class="statistic-card" @click="goToPage('/medicationmanagement/statisticalManagement/medicationUsageDetails')">
<div class="card-content">
<div class="card-icon">
<i class="el-icon-trend-charts" style="font-size: 36px; color: #67C23A;"></i>
</div>
<div class="card-text">
<h3>药品使用情况</h3>
<p>查看药品使用统计</p>
</div>
</div>
</el-card>
<!-- 药品销售情况 -->
<el-card class="statistic-card" @click="goToPage('/medicationmanagement/statisticalManagement/medicationSaleDetails')">
<div class="card-content">
<div class="card-icon">
<i class="el-icon-sell" style="font-size: 36px; color: #F56C6C;"></i>
</div>
<div class="card-text">
<h3>药品销售情况</h3>
<p>查看药品销售统计</p>
</div>
</div>
</el-card>
<!-- 采购入库明细 -->
<el-card class="statistic-card" @click="goToPage('/medicationmanagement/statisticalManagement/medicationInboundDetails')">
<div class="card-content">
<div class="card-icon">
<i class="el-icon-shopping-cart-full" style="font-size: 36px; color: #909399;"></i>
</div>
<div class="card-text">
<h3>采购入库明细</h3>
<p>查看采购入库统计</p>
</div>
</div>
</el-card>
<!-- 出库明细 -->
<el-card class="statistic-card" @click="goToPage('/medicationmanagement/statisticalManagement/stockOutDetail')">
<div class="card-content">
<div class="card-icon">
<i class="el-icon-shopping-cart-1" style="font-size: 36px; color: #409EFF;"></i>
</div>
<div class="card-text">
<h3>出库明细</h3>
<p>查看药品出库统计</p>
</div>
</div>
</el-card>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
const goToPage = (path) => {
router.push(path);
};
</script>
<style lang="scss" scoped>
.statistics-portal {
padding: 20px;
.portal-title {
text-align: center;
margin-bottom: 10px;
color: #303133;
}
.portal-description {
text-align: center;
color: #909399;
margin-bottom: 30px;
}
.statistics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.statistic-card {
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.card-content {
display: flex;
align-items: center;
.card-icon {
margin-right: 15px;
}
.card-text {
flex: 1;
h3 {
margin: 0 0 5px 0;
font-size: 16px;
color: #303133;
}
p {
margin: 0;
font-size: 14px;
color: #909399;
}
}
}
}
}
</style>