ea15a94b09d6d6b0c62f64aa6cfa03b4636cf557..4ca4444ad33a2a1a51f5c06e78ee9365dfb1eed6
2026-10-08 liu
feat(mes): 种子二维码标签国标四要素合规改造
4ca444 对比 | 目录
2026-10-08 liu
Merge remote-tracking branch 'origin/dev_新疆_塔河种业' into dev_新疆_塔河种业
975fe5 对比 | 目录
2026-09-30 云
feat(mes): 更新追溯功能支持未绑定批次和品种单位信息
8ea920 对比 | 目录
2026-09-30 tuqin
feat: 业务编码改为随机16位数字并保证全系统唯一
0ddbb4 对比 | 目录
2026-09-29 tuqin
fix(mes): 移除变基残留的 setCapacity 调用,修复编译报错
f4b6c4 对比 | 目录
2026-09-29 tuqin
feat: 业务编码统一为16位纯数字并保证全系统唯一
7580d0 对比 | 目录
2026-09-28 云
chore(config): 更新测试环境配置文件中的自动配置禁用列表
eef512 对比 | 目录
2026-09-24 云
config(database): 更新数据库连接端口配置
617806 对比 | 目录
2026-09-24 云
chore(config): 更新服务器地址和部署配置
04795d 对比 | 目录
2026-09-17 云
feat(mes): 重构袋码生成与物料分类功能
f72732 对比 | 目录
2026-09-16 云
feat(trace): 添加企业简介模块
b18066 对比 | 目录
2026-09-16 云
feat(trace): 添加种子流向信息展示功能
c89f7d 对比 | 目录
2026-09-15 云
config(mes): 更新溯源页面基础地址配置
602993 对比 | 目录
2026-09-14 云
fix(config): 修复滑动验证码底图路径配置及数据库连接参数调整
b9cddc 对比 | 目录
2026-09-09 云
config(mes): 更新溯源页面基础地址端口
41ee9b 对比 | 目录
2026-09-09 云
chore(config): 更新溯源页面基础地址配置
1c4835 对比 | 目录
2026-09-03 云
docs(mes): 添加库存现有量导入功能前端联调文档
93c5e5 对比 | 目录
2026-09-02 云
chore(tests): 删除 Playwright 测试相关文件
e68084 对比 | 目录
2026-09-02 云
refactor(mes): 优化MES生产扫码入库功能支持多类型扫码
1ba0db 对比 | 目录
已修改129个文件
已删除4个文件
已添加49个文件
7193 ■■■■■ 文件已修改
.gitignore 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/bagcode_home_dashboard_integration.md 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/batch_trace_refactor_integration.md 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/h5_trace_item_unit_integration.md 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/h5_trace_unbound_batch_integration.md 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_item_remove_category_purchase_price_integration.md 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_unit_measure_type_integration.md 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_bag_code_package_refactor_integration.md 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_bag_code_package_refactor_requirements.md 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_pro_batch_processing_team_integration.md 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_wm_material_stock_bag_api.md 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sales_notice_to_product_sales.md 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/scan_in_multi_type_inbound.md 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/seed_flow_trace_integration.md 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/seed_qr_label_gb_compliance_integration.md 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/seed_quality_bag_granularity.md 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/pro_batch_processing_team.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/trace_platform_01_schema.sql 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/wm_transaction_query.md 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
img/0d9430aac760f2a00123f8f4f9b8eae0.jpg 补丁 | 查看 | 原始文档 | blame | 历史
img/8fbfd97ac926a2dfd7e3bc2577aee946.jpg 补丁 | 查看 | 原始文档 | blame | 历史
img/ChatGPT Image 2026年9月30日 18_10_01.png 补丁 | 查看 | 原始文档 | blame | 历史
img/tahe_seed_logo.png 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_pro_bag_code_qc_status.sql 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_pro_scan_event_scan_type.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_qc_seed_quality_record_menu.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_wm_material_stock_bag.sql 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_wm_product_sales_add_columns.sql 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_wm_qualified_warehouse.sql 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_wm_transaction_bag_code.sql 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/sp_clean_business_data.sql 435 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/wls_sales_outbound_menu_extract.sql 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/BizNoUtils.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/unit/dto/MdmUnitMeasureRespDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemRespVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemSyncReqDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApi.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesCreateReqDTO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/salesnotice/MesWmSalesNoticeApi.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApiImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/salesnotice/MesWmSalesNoticeApiImpl.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/home/vo/MesBagCodeHomeSummaryRespVO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodePageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRawGenerateReqVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRespVO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchPageReqVO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchRespVO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchSaveReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/MesProPalletController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletBatchCreateReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletPreGenerateReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/MesProScanEventController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/vo/MesProScanEventCreateReqVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/vo/MesProScanEventRespVO.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/trace/MesProTraceResolveController.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityPageReqVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualitySubmitReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/trace/batch/MesTraceBatchController.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchFlowRespVO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockBagPageReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockBagRespVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockListReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesPageReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/MesWmSalesNoticeController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/MesWmSalesNoticePageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/MesWmSalesNoticeRespVO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/MesWmSalesNoticeSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/line/MesWmSalesNoticeLineRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/line/MesWmSalesNoticeLineSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehousePageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehouseRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehouseSaveReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/MesProTracePublicController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/vo/MesProTracePublicConfigRespVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/vo/MesProTracePublicRespVO.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeDO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/batch/MesProBatchDO.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/pallet/MesProPalletDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/scan/MesProScanEventDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/batch/MesWmBatchDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/materialstockbag/MesWmMaterialStockBagDO.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productsales/MesWmProductSalesDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productsales/MesWmProductSalesLineDO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/salesnotice/MesWmSalesNoticeDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/salesnotice/MesWmSalesNoticeLineDO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/sn/MesWmSnDO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/transaction/MesWmTransactionDO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warehouse/MesWmWarehouseDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/home/MesBagCodeHomeStatisticsMapper.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/batch/MesProBatchMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstockbag/MesWmMaterialStockBagMapper.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesLineMapper.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/returnsales/MesWmReturnSalesMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/salesnotice/MesWmSalesNoticeMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/warehouse/MesWmWarehouseMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesBizTypeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProBagCodeQcStatusEnum.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/trace/MesTraceCodeStatusEnum.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmMaterialStockBagStatusEnum.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmWarehouseTypeEnum.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/config/MesProperties.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/config/MesQrCodeConfig.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/MesProQrLabelBuilder.java 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/home/MesBagCodeHomeStatisticsServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodeRecordServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/pallet/MesProPalletService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/pallet/MesProPalletServiceImpl.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventServiceImpl.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstockbag/MesWmMaterialStockBagService.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstockbag/MesWmMaterialStockBagServiceImpl.java 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeLineServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warehouse/MesWmWarehouseService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warehouse/MesWmWarehouseServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/home/MesBagCodeHomeStatisticsMapper.xml 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/java/cn/iocoder/yudao/server/framework/bizno/BizCodeSetInitializer.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-local.yaml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-test.yaml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-thzy.yaml 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application.yaml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/static/trace.html 895 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -57,4 +57,6 @@
### Claude Code ###
.claude/settings.local.json
.claude/worktrees/
.claude/scheduled_tasks.json
.claude/scheduled_tasks.json
.export_tmp/
docs/bagcode_home_dashboard_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
# ä¸€è¢‹ä¸€ç é¦–页改版(全链路驾驶舱) â€” å‰ç«¯è”调方案
## èƒŒæ™¯
将 `/bagcode-home` é¦–页从"基础数据 + ä¸€è¢‹ä¸€ç ç”Ÿäº§"的局部统计页,重构为覆盖**完整价值链**的驾驶舱:品种 â†’ äº§çº¿/批次 â†’ è¢‹ç /托盘 â†’ é”€å”®å‡ºåº“ â†’ æµé€šæ‰«ç  â†’ é˜²çªœæº¯æº,并串联仓储库存、质量预警、快捷入口。整体视觉对齐登录页的**农业翠绿玻璃**风格。
## æ¶‰åŠé¡µé¢
- é¦–页(驾驶舱):`src/views/dashboard/bagCodeHome/index.vue` â€”— å…¨é“¾è·¯é‡æž„
- å…³è”跳转页面:基础数据、一袋一码生产、出库流通、防伪防窜货、溯源分析、仓储物流、质量管理 ä¸‹çš„各业务页
## æŽ¥å£å˜æ›´
### 1. æ‰©å±•汇总接口(返回新增字段)
`GET /mes/bag-code-home-statistics/summary`
新增响应字段(均 `Long`,无默认值时返回 0):
| å­—段 | è¯´æ˜Ž | æ•°æ®æ¥æºè¡¨ |
|------|------|-----------|
| `salesNoticeCount` | å‘货通知单数 | `mes_wm_sales_notice` |
| `productSalesCount` | é”€å”®å‡ºåº“单数 | `mes_wm_product_sales` |
| `returnSalesCount` | é”€å”®é€€è´§å•æ•° | `mes_wm_return_sales` |
| `stockWarningPendingCount` | æœªå¤„理库存预警数(`status=10`) | `mes_wm_stock_warning` |
| `ncrPendingCount` | å¾…评审不合格品单数(`status=0`) | `mes_qc_ncr` |
原有字段(`mdmItemCount/mesItemCount/itemTypeCount/unitMeasureCount/lineCount/batchCount/bagCodeCount/palletCount/batchStatusList/bagCodeStatusList`)保持不变。
### 2. å¤ç”¨å·²æœ‰ç»Ÿè®¡æŽ¥å£(无需改动)
| æŽ¥å£ | ç”¨é€” | å‰ç«¯ API |
|------|------|---------|
| `GET /mes/trace-cross-region/summary` | çªœè´§é¢„警汇总(`total/pending/handled/falseAlarm/crossProvince/crossCity`) | `api/mes/trace-cross-region`(新增) |
| `GET /mes/trace-consumer-scan/summary` | ç´¯è®¡æ‰«ç /成功/失败/涉及批次 | å·²æœ‰ |
| `GET /mes/trace-consumer-scan/distribution?groupBy=province` | æ¶ˆè´¹è€…扫码地区分布 | å·²æœ‰ |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. é¦–页结构(index.vue)
| åŒºå— | ç»„ä»¶ | æ•°æ® |
|------|------|------|
| ä¸šåŠ¡å…¨é“¾è·¯ | `modules/business-flow.vue` | summary + æ‰«ç æ€»æ•° + çªœè´§æ€»æ•° |
| å…¨é“¾è·¯ KPI æ€»è§ˆ(8 å¡) | `modules/kpi-cards.vue` | summary + æ‰«ç æ€»æ•° |
| è¢‹ç ç”Ÿæˆè¶‹åŠ¿ | `modules/trend-chart.vue`(复用) | `summary/trend` |
| æ¶ˆè´¹è€…扫码地区分布 | `modules/scan-region-chart.vue`(新增) | `distribution/groupBy=province` |
| æ‰¹æ¬¡çŠ¶æ€åˆ†å¸ƒ / è¢‹ç çŠ¶æ€åˆ†å¸ƒ | `batch-status-chart.vue` / `bag-code-status-chart.vue`(复用) | summary çŠ¶æ€åˆ—è¡¨ |
| å¾…办与预警 | `modules/alert-panel.vue`(新增) | summary + çªœè´§æ±‡æ€» |
| åˆ†ç»„快捷入口 | `modules/shortcuts.vue`(重写为分组) | é™æ€è·¯ç”±é…ç½® |
### 2. æ–°å¢ž/删除文件
- æ–°å¢ž:`api/mes/trace-cross-region/index.ts`
- æ–°å¢ž:首页 `modules/business-flow.vue`、`kpi-cards.vue`、`scan-region-chart.vue`、`alert-panel.vue`
- é‡å†™:首页 `index.vue`、`modules/shortcuts.vue`
- æ›´æ–°:`api/mes/bag-code-home/index.ts`(Summary åŠ  5 å­—段)、`data.ts`、`chart-options.ts`
- åˆ é™¤:`modules/basic-cards.vue`、`modules/bag-code-cards.vue`(不再使用)
### 3. æ ·å¼ä¸»é¢˜
- é¦–页内容区统一"农业翠绿玻璃"主题:浅色翠绿渐变背景(无背景图)、白色磨砂玻璃卡片、薄荷白/墨绿文字。
- å›¾è¡¨ç»Ÿä¸€ç¿ ç»¿ä¸»è‰²(`#10b981`)并适配浅底文字色。
- ä¸»é¢˜æ ·å¼é›†ä¸­åœ¨ `index.vue` çš„非 scoped `<style>` ä¸­(`.agri-dashboard` å®¹å™¨ä¸ºä½œç”¨åŸŸ),各卡片组件使用对应玻璃类。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å“ç§æ€»æ•° | KPI + ä¸šåŠ¡é“¾è·¯ | ç‚¹æŒ‰è·³è½¬ `MdmItem` |
| ç”Ÿäº§æ‰¹æ¬¡ | KPI + ä¸šåŠ¡é“¾è·¯ | è·³è½¬ `MesProBatch` |
| è¢‹ç  / æ‰˜ç›˜ç  | KPI + ä¸šåŠ¡é“¾è·¯ | è·³è½¬ `MesProBagCode` / `MesProPallet` |
| é”€å”®å‡ºåº“单数 | KPI + ä¸šåŠ¡é“¾è·¯ | è·³è½¬ `MesWmProductSales` |
| ç´¯è®¡æ‰«ç  | KPI + ä¸šåŠ¡é“¾è·¯ | æ¥è‡ª `trace-consumer-scan/summary`,跳转 `MesConsumerScanStatistics` |
| çªœè´§é¢„警总数 | ä¸šåŠ¡é“¾è·¯(防窜/溯源) | æ¥è‡ª `trace-cross-region/summary`,跳转 `MesTraceCrossRegion` |
| åº“存预警(未处理) | KPI + å¾…办预警 | è·³è½¬ `MesWmStockWarning` |
| å¾…评审不合格 | KPI + å¾…办预警 | è·³è½¬ `MesQcNcr` |
## æ³¨æ„äº‹é¡¹
- æ–°å¢žçš„ 5 ä¸ªæ±‡æ€»å­—段需要**后端重新编译并重启**后才会返回;重启前前端已做 `?? 0` å…œåº•,相关卡片/节点显示 0,不影响其它数据。
- è·¯ç”±è·³è½¬ä½¿ç”¨**路由 name**(已与菜单 `component_name` å¯¹é½),未加载的菜单沿用 `message.warning('当前菜单尚未加载...')` å…œåº•。
- æ¶ˆè´¹è€…扫码/窜货无经纬度字段,仅支持省/市文本分布图,不支持地图坐标。
docs/batch_trace_refactor_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
# æ‰¹æ¬¡è¿½æº¯ç»Ÿä¸€èšåˆæŽ¥å£ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- MES â†’ è´¨æ£€ â†’ æ‰¹æ¬¡è¿½æº¯ï¼ˆ`/qc/batch-trace`)
- æ‰¹æ¬¡è¿½æº¯è¯¦æƒ…
- æ‰¹æ¬¡ç ã€æ‰˜ç›˜ç ã€è¢‹ç æ‰«ç è¿½æº¯å…¥å£
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. åœ¨æ‰¹æ¬¡è¿½æº¯åˆ—表输入生产批次码、临时批次码、袋码或托盘码。
2. é¡µé¢è°ƒç”¨ç»Ÿä¸€è¯¦æƒ…接口,后端将入口码解析到生产批次,并聚合生产、质检、仓储、销售和防伪信息。
3. è¯¦æƒ…页以生产批次为根展示全链路数据;袋码和托盘信息来自生产赋码数据,仓储与销售信息来自仓储业务数据。
4. å‰å‘追溯展示当前批次产出的下游批次;后向追溯展示当前批次消耗的上游批次。
5. æ—¶é—´çº¿ã€é˜¶æ®µè§†å›¾å’Œå†³ç­–摘要均使用同一次聚合响应,避免打开详情后重复请求多个旧接口。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|---|---|---|
| GET | `/mes/trace/batch/detail` | æŸ¥è¯¢ç»Ÿä¸€æ‰¹æ¬¡è¿½æº¯è¯¦æƒ… |
### è¯·æ±‚参数
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|---|---|---|---|
| `code` | String | æ˜¯ | ç”Ÿäº§æ‰¹æ¬¡ç ã€ä¸´æ—¶æ‰¹æ¬¡ç ã€è¢‹ç æˆ–托盘码 |
示例:`code=BAG-202608280001`
### å“åº”主要字段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|---|---|---|
| `sourceType` | String | å…¥å£ç±»åž‹ï¼š`BAG`、`PALLET`、`BATCH` |
| `requestCode` | String | åŽŸå§‹è¾“å…¥ç  |
| `itemCode` / `itemName` | String | å“ç§ç¼–码、名称 |
| `itemSpecification` | String | å“ç§è§„æ ¼ |
| `batchCode` / `tempCode` | String | æ­£å¼æ‰¹æ¬¡å·ã€ä¸´æ—¶æ‰¹æ¬¡ç  |
| `batchStatusName` | String | æ‰¹æ¬¡çŠ¶æ€åç§° |
| `produceDate` | DateTime | ç”Ÿäº§æ—¥æœŸ |
| `lineCode` / `lineName` | String | äº§çº¿ç¼–码、名称 |
| `ownerUserName` | String | ç”Ÿäº§è´£ä»»äºº |
| `planQuantity` | Decimal | è®¡åˆ’数量 |
| `bagQuantity` / `palletQuantity` | Integer | è¢‹ç æ•°é‡ã€æ‰˜ç›˜æ•°é‡ |
| `workers` | Array | ç”Ÿäº§äººå‘˜ |
| `pallets` | Array | æ‰˜ç›˜æ‘˜è¦ |
| `bagTotal` / `bags` | Long / Array | è¢‹ç æ€»æ•°åŠæœ€å¤š 100 æ¡æ˜Žç»† |
| `inbound` | Object | æœ€æ–°å…¥åº“事件 |
| `quality` | Object | ç§å­è´¨é‡æ£€éªŒä¸ŽæŒ‡æ ‡ |
| `antiFake` | Object | å½“前追溯码扫码次数、首次和最近扫码时间 |
| `purchaseSources` | Array | é‡‡è´­æ¥æºã€ä¾›åº”商和 IQC ä¿¡æ¯ |
| `salesTargets` | Array | é”€å”®å‡ºåº“、客户和 OQC ä¿¡æ¯ |
| `processTraces` | Array | å·¥åºæŠ•入、产出流转信息 |
### å“åº”示例
```json
{
  "code": 0,
  "data": {
    "sourceType": "BAG",
    "requestCode": "BAG-202608280001",
    "batchCode": "B20260828001",
    "itemName": "示例品种",
    "batchStatusName": "已完成",
    "workers": [],
    "pallets": [],
    "bags": [],
    "purchaseSources": [],
    "salesTargets": [],
    "processTraces": []
  }
}
```
## é¡µé¢å±•示规则
| è§†å›¾ | å±•示内容 |
|---|---|
| é˜¶æ®µæ€»è§ˆ | æ¥æºã€ç”Ÿäº§ã€è´¨æ£€ã€èµ‹ç ã€å…¥åº“、销售、消费、防伪八个阶段的状态和数量 |
| å…¨é“¾è·¯æ—¶é—´çº¿ | æŒ‰æ—¶é—´å±•示采购、生产、质检、赋码、入库、出库和扫码事件;异常状态优先高亮 |
| æµåŠ¨å›¾è°± | ä»¥æ‰¹æ¬¡ã€å·¥åºã€ä»“库、托盘、袋码、销售单和客户为节点;默认展示批次级关系 |
| å†³ç­–分析 | å±•示质量结论、库存数量、销售去向、防伪扫码次数和窜货风险 |
| é‡‡è´­æ¥æº | å±•示采购入库、供应商、采购订单、IQC å•号及检验结论 |
| é”€å”®å޻向 | å±•示销售出库、客户、销售订单、OQC å•号及质量状态 |
| å·¥åºæµè½¬ | `IN` æ˜¾ç¤ºä¸ºæŠ•入,`OUT` æ˜¾ç¤ºä¸ºäº§å‡º |
## ä¸šåŠ¡è§„åˆ™
- è¾“入袋码或托盘码时,详情仍以其所属生产批次为根进行聚合。
- å½“前接口返回的袋码明细最多 100 æ¡ï¼Œ`bagTotal` ç”¨äºŽå±•示完整数量;大批次不得按明细数量判断总量。
- ç©ºæ•°ç»„表示该业务阶段暂无记录;`quality`、`inbound`、`antiFake` ä¸ºç©ºè¡¨ç¤ºæš‚无对应信息。
- é˜²ä¼ªæ‰«ç ç»Ÿè®¡ä»…用于展示,不能替代质量检验结论或出库状态。
- å®¢æˆ·ã€ä¾›åº”商、操作人等名称应以接口返回值为准,不在前端重复查询旧批次接口。
## æ³¨æ„äº‹é¡¹
- ç®¡ç†ç«¯æŽ¥å£éœ€è¦æ‰¹æ¬¡è¿½æº¯æŸ¥è¯¢æƒé™ï¼š`mes:trace-batch:query`。
- æ—§çš„ `/mes/wm/batch/*` æŽ¥å£ç»§ç»­ä¾›ä»“储批次页面使用,批次追溯页面切换后不应再并行请求旧的五个详情接口。
- åŽç»­æ‰©å±•时间线和图谱时,建议后端增加分页与节点层级参数,避免一次性加载全部袋码。
- å±•示消费者扫码信息时不得展示 IP åœ°å€å’Œ User-Agent åŽŸæ–‡ã€‚
docs/h5_trace_item_unit_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
# H5 æº¯æºã€Œå“ç§ä¿¡æ¯å¸¦è®¡é‡å•位」- å‰ç«¯è”调说明
## èƒŒæ™¯
H5 æ‰«ç æº¯æºé¡µï¼ˆ`mes.trace-base-url` æŒ‡å‘çš„ `trace.html`)「追溯查询」区块的品种信息需要带上计量单位。
品种主计量单位存储在 `mdm_item.unit_measure_id`(单位编号),页面需要的单位名称由后端通过
`MdmUnitMeasureApi.getUnitMeasure(id)` è§£æžåŽéšå“åº”返回,前端不需要再单独请求单位接口。
同时按需求精简「追溯查询」区块:去掉「责任人」「质检结论」两行(这两项数据在折叠的「质检信息」区块中仍然可查,未丢失)。
## æ¶‰åŠæŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /public/mes/trace/query?code={追溯码} | H5 æº¯æºæŸ¥è¯¢ï¼ˆå…ç™»å½•),响应品种区块新增 `itemUnitMeasureName` |
**响应新增字段(品种信息区块,位于 `itemSpecification` ä¹‹åŽï¼‰ï¼š**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| itemUnitMeasureName | String | å“ç§ä¸»è®¡é‡å•位名称(如 `48千克`);物料未配置单位时返回 `null` |
**响应示例(品种相关片段):**
```json
{
  "code": 0,
  "data": {
    "itemCode": "ITEM_20260917002",
    "itemName": "测试",
    "itemSpecification": "一级",
    "itemUnitMeasureName": "48千克"
  }
}
```
## é¡µé¢å±•示规则
「追溯查询」区块字段顺序:
| å­—段 | å–值 | è¯´æ˜Ž |
|------|------|------|
| å“ç§åç§° | `itemName` + `itemSpecification` | ä¸¤è€…以空格拼接,为空的不参与拼接 |
| è®¡é‡å•位 | `itemUnitMeasureName` | æœªé…ç½®å•位时展示 `-` |
| ç”Ÿäº§ç»è¥è€… | é¡µé¢å¸¸é‡ `COMPANY_NAME` | é™æ€ä¿¡æ¯ |
| å•元识别代码 | `requestCode` â†’ `batchCode` â†’ `tempCode` ä¾æ¬¡å›žé€€ | â€” |
| è¿½æº¯ç½‘址 | é¡µé¢å¸¸é‡ `TRACE_URL` | å¤–网访问地址,与包装/二维码印制一致 |
| è¿½æº¯ç±»åž‹ | `sourceType` | è¢‹ç  / æ‰˜ç›˜ç  / æ‰¹æ¬¡ç  |
| æ‰¹æ¬¡å· | `batchCode` | â€” |
| ç”Ÿäº§æ—¥æœŸ | `produceDate` | â€” |
| äº§çº¿ | `lineName`,为空回退 `lineCode` | â€” |
**本次移除的展示项:**
| å­—段 | åŽŸå±•ç¤ºä½ç½® | è¯´æ˜Ž |
|------|-----------|------|
| è´£ä»»äºº | è¿½æº¯æŸ¥è¯¢åŒºå— | å·²ç§»é™¤ï¼Œ`ownerUserName` å­—段仍在响应中 |
| è´¨æ£€ç»“论 | è¿½æº¯æŸ¥è¯¢åŒºå— | å·²ç§»é™¤ï¼Œè´¨æ£€ç»“论仍在响应 `quality.resultName` ä¸­ï¼Œå¯åœ¨æŠ˜å çš„「质检信息」区块查看 |
## æ³¨æ„äº‹é¡¹
- `itemUnitMeasureName` å–的是单位**名称**(`mdm_unit_measure.name`),不是单位类型(`type`,如 48千克/100千克/吨)。物料未配置主计量单位时该字段为 `null`,页面展示 `-`。
- å•位名称为空字符串时后端统一转成 `null` è¿”回。
- è¯¥å­—段为纯展示字段,历史数据无需处理;接口其余字段与结构保持不变,向后兼容。
docs/h5_trace_unbound_batch_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
# H5 æº¯æºã€Œæœªç»‘定批次也可扫码」- å‰ç«¯è”调说明
## èƒŒæ™¯
袋码 / æ‰˜ç›˜ç åœ¨ç”ŸæˆåŽã€å½•入生产批次前,是一个「已印制、未投产」的中间态。此前该状态下调用溯源查询接口会直接返回错误码 `1040760000`(未查询到该追溯码),消费者扫到这类包装时无法查看任何信息。
本次调整:**未绑定批次的袋码 / æ‰˜ç›˜ç åŒæ ·å¯ä»¥æ‰«ç æˆåŠŸ**,接口正常返回该码对应的**品种信息**,并新增 `batchBound` å­—段标识批次是否已录入,由前端提示「尚未绑定批次」。追溯码本身不存在时,行为不变,仍返回 `1040760000`。
## æ¶‰åŠæŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /public/mes/trace/query?code={追溯码} | H5 æº¯æºæŸ¥è¯¢ï¼ˆå…ç™»å½•) |
## å“åº”新增字段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| batchBound | Boolean | æ˜¯å¦å·²å½•入生产批次。`true` å·²ç»‘定(批次区块字段正常返回);`false` æœªç»‘定,此时**仅** `itemCode` / `itemName` / `itemSpecification` / `itemUnitMeasureName`、`sourceType`、`requestCode`、`antiFake`、`timeline`、`decision` æœ‰å€¼ï¼Œå…¶ä½™æ‰¹æ¬¡ç›¸å…³å­—段全部为 `null` |
**字段位置:** `requestCode` ä¹‹åŽï¼Œ`itemCode` ä¹‹å‰ã€‚
## å˜æ›´å‰åŽå¯¹æ¯”
| åœºæ™¯ | å˜æ›´å‰ | å˜æ›´åŽ |
|------|--------|--------|
| è¢‹ç å·²ç”Ÿæˆã€æœªå½•入批次 | `code=1040760000`,`data=null`,无法展示任何信息 | `code=0`,`batchBound=false`,返回品种信息,前端提示未绑定批次 |
| æ‰˜ç›˜å·²ç”Ÿæˆã€æœªå½•入批次 | åŒä¸Š | åŒä¸Šï¼Œå“ç§å–托盘内**任一袋码**的品种 |
| è¿½æº¯ç ä¸å­˜åœ¨ | `code=1040760000` | ä¸å˜ |
## å“åº”示例(未绑定批次)
```json
{
  "code": 0,
  "data": {
    "sourceType": "BAG",
    "requestCode": "BAG-00000153",
    "batchBound": false,
    "itemCode": "ITEM_20260917003",
    "itemName": "吨",
    "itemSpecification": null,
    "itemUnitMeasureName": "吨袋子",
    "batchCode": null,
    "tempCode": null,
    "batchStatus": null,
    "batchStatusName": null,
    "produceDate": null,
    "lineCode": null,
    "lineName": null,
    "ownerUserId": null,
    "ownerUserName": null,
    "planQuantity": null,
    "bagQuantity": null,
    "palletQuantity": null,
    "inboundTime": null,
    "remark": null,
    "workers": null,
    "palletCount": null,
    "pallets": null,
    "bagTotal": null,
    "bags": null,
    "inbound": null,
    "quality": null,
    "antiFake": {
      "scanCount": 0,
      "firstScanTime": null,
      "lastScanTime": null
    },
    "purchaseSources": null,
    "salesTargets": null,
    "seedFlows": null,
    "processTraces": null,
    "timeline": [],
    "decision": {
      "qualityRisk": false,
      "inboundQuantity": null,
      "salesTargetCount": 0,
      "consumerScanCount": 0,
      "riskLevel": "NORMAL"
    }
  }
}
```
## é¡µé¢å±•示规则
### è¿½æº¯æŸ¥è¯¢åŒºå—
| å­—段 | ç»‘定批次(`batchBound=true`) | æœªç»‘定批次(`batchBound=false`) |
|------|------------------------------|--------------------------------|
| å“ç§åç§° | `itemName` + `itemSpecification` ç©ºæ ¼æ‹¼æŽ¥ | åŒå·¦ |
| è®¡é‡å•位 | `itemUnitMeasureName` | åŒå·¦ |
| ç”Ÿäº§ç»è¥è€… | é¡µé¢å¸¸é‡ `COMPANY_NAME` | åŒå·¦ |
| å•元识别代码 | `requestCode` â†’ `batchCode` â†’ `tempCode` ä¾æ¬¡å›žé€€ | `requestCode` |
| è¿½æº¯ç½‘址 | é¡µé¢å¸¸é‡ `TRACE_URL` | åŒå·¦ |
| è¿½æº¯ç±»åž‹ | `sourceType` | åŒå·¦ |
| æ‰¹æ¬¡å· | `batchCode` | **不展示**,替换为提示文案 |
| ç”Ÿäº§æ—¥æœŸ | `produceDate` | **不展示** |
| äº§çº¿ | `lineName`,为空回退 `lineCode` | **不展示** |
**未绑定批次时的提示文案(替代批次号 / ç”Ÿäº§æ—¥æœŸ / äº§çº¿ä¸‰è¡Œï¼‰ï¼š**
> è¯¥è¿½æº¯ç å°šæœªå½•入生产批次,批次号、生产日期、产线等信息待完善;当前展示该码对应的品种信息。
### é˜²ä¼ªæŸ¥è¯¢åŒºå—
- `batchCode` æœ‰å€¼æ—¶ï¼Œ`对应批次` è¡Œå±•示批次号。
- `batchCode` ä¸ºç©ºï¼ˆå³æœªç»‘定批次)时,`对应批次` è¡Œå±•示「尚未绑定批次」,并以弱化 / è­¦ç¤ºæ ·å¼åŒºåˆ†ï¼Œé¿å…è¢«è¯¯è¯»ä¸ºã€Œæ— æ•°æ®ã€ã€‚
## æ³¨æ„äº‹é¡¹
- `batchBound` æ˜¯**唯一**判断批次是否已录入的字段,前端不要用 `batchCode == null` æŽ¨æ–­ï¼ˆåŽ†å²æ•°æ®ä¸­åŒæ ·å­˜åœ¨æ‰¹æ¬¡å·ä¸ºç©ºä½†æ‰¹æ¬¡å·²ç»‘å®šçš„è¾¹ç•Œæƒ…å†µï¼‰ã€‚
- æœªç»‘定批次时,`bags` / `pallets` / `workers` / `quality` ç­‰åŒºå—均为 `null`(不是空数组),前端渲染前需判空。
- æœªç»‘定批次的扫码**同样会**计入防伪查询次数(`antiFake.scanCount`)与消费者扫码事件,逻辑与正常扫码一致。
- æ‰˜ç›˜çš„品种取「托盘内任一袋码」的品种。理论上同一托盘的袋码品种一致;若出现不一致(数据异常),展示的品种可能与实际不符,属可接受的降级表现。
- è¿½æº¯ç æœ¬èº«ä¸å­˜åœ¨æ—¶ä»è¿”回 `code=1040760000`,前端沿用原有错误提示,不需要改动。
- æœ¬æ¬¡ä¸º**放宽**校验,不影响已绑定批次的历史数据,接口向后兼容。
docs/mdm_item_remove_category_purchase_price_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
# å“ç§ç®¡ç†åŽ»æŽ‰ã€Œå“ç§åˆ†ç±»ã€ã€Œé‡‡è´­ä»·ã€ - å‰ç«¯è”调方案
## èƒŒæ™¯
基础数据-品种管理不再维护「品种分类」和「采购价」两个字段:
- æ•°æ®åº“列 `mdm_item.category_id`、`mdm_item.purchase_price` ä¿ç•™ï¼ˆåŽ†å²æ•°æ®ä¸åˆ ï¼‰ï¼Œä½†æŽ¥å£å±‚ä¸å†æŽ¥æ”¶å’Œè¿”å›žã€‚
- MDM→MES åŒæ­¥ä¸å†ä¼ å“ç§åˆ†ç±»ï¼ŒåŒæ­¥ç”Ÿæˆçš„ MES ç‰©æ–™ã€Œç‰©æ–™åˆ†ç±»ã€ä¸ºç©ºï¼Œå¯åœ¨ MES ç‰©æ–™äº§å“é¡µé¢æ‰‹å·¥è¡¥å¡«ã€‚
- MES→MDM åå‘同步(MES ç‰©æ–™å‹¾é€‰ã€ŒåŒæ­¥åˆ° MDM」回写分类)保持不变。
## API å˜åŒ–
| æ–¹æ³• | è·¯å¾„ | å˜åŒ– |
|------|------|------|
| POST | /mdm/item/create | è¯·æ±‚体不再接收 `categoryId`、`purchasePrice`(传入将被忽略) |
| PUT | /mdm/item/update | åŒä¸Š |
| GET | /mdm/item/get | å“åº”不再返回 `categoryId`、`categoryName`、`purchasePrice` |
| GET | /mdm/item/page | å“åº”不再返回上述三字段;不再支持 `categoryId` æŸ¥è¯¢å‚æ•° |
| GET | /mdm/item/simple-list | å“åº”不再返回上述三字段 |
| GET | /mdm/item/list-by-ids | å“åº”不再返回上述三字段 |
**响应示例(节选):**
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "code": "PZ0001",
        "name": "水稻品种A",
        "specification": "500g/袋",
        "unitMeasureId": 2,
        "unitMeasureName": "袋",
        "salesPrice": 15.00,
        "syncedMes": true
      }
    ],
    "total": 1
  }
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹ï¼ˆæœ¬æ‰¹å·²åŒæ­¥ä¿®æ”¹ï¼Œä¾›å‰ç«¯å›¢é˜Ÿå¤æ ¸ï¼‰
### 1. å“ç§ç®¡ç†é¡µé¢ï¼ˆsrc/views/basicData/mdm/)
| æ–‡ä»¶ | ä¿®æ”¹å†…容 |
|------|----------|
| data.ts | åˆ é™¤è¡¨å•「品种分类」TreeSelect、「采购价」InputNumber;删除列表「品种分类」「采购价」列;删除 `buildCategoryTree` åŠ MES ç‰©æ–™åˆ†ç±»æŽ¥å£ import |
| index.vue | åˆ é™¤åˆ†ç±»åˆ—表缓存、分类名称/颜色工具函数、`#categoryName` æ’槽及分类加载逻辑 |
| modules/form.vue | åˆ é™¤æ‰“开表单时加载分类树的逻辑 |
| components/data.ts | å“ç§é€‰æ‹©å¼¹çª—删除「品种分类」搜索项与列 |
| components/select-dialog.vue | åˆ é™¤åˆ†ç±»ç¼“存、名称/颜色函数、`#categoryId` æ’槽及分类加载逻辑 |
### 2. API ç±»åž‹ï¼ˆsrc/api/mdm/item/index.ts)
- `MdmItemApi.Item` åˆ é™¤ `categoryId`、`categoryName`、`purchasePrice`。
- `MdmItemApi.PageParams` åˆ é™¤ `categoryId`。
### 3. ä¾èµ–采购价自动算价的单据页(行为变化,重点复核)
品种不再有采购价,以下两处选品后「单价」不再自动带入,改为默认 0、由用户手工填写:
| æ–‡ä»¶ | ä¿®æ”¹å†…容 |
|------|----------|
| src/views/erp/purchase/order/modules/item-form.vue | é€‰æ‹©å“ç§åŽ `productPrice` ä¸å†å–采购价,置 0 æ‰‹å·¥å¡«å†™ |
| src/views/erp/purchase/request/modules/item-form.vue | åŒä¸Š |
其余单据(销售、库存等)使用的是 ERP äº§å“ï¼ˆ`erp_product`)自身价格或销售价,不受影响。
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ–°å¢ž/编辑品种 | æ— åˆ†ç±»ã€æ— é‡‡è´­ä»·å­—段;保存接口忽略这两个入参 |
| ç¼–辑历史品种 | åŽ†å²å·²å¡«çš„åˆ†ç±»/采购价在更新时不会被清空(仅不再回显),也不会被覆盖 |
| å“ç§åŒæ­¥åˆ° MES | ç”Ÿæˆçš„ MES ç‰©æ–™äº§å“ã€Œç‰©æ–™åˆ†ç±»ã€ä¸ºç©ºï¼›MES ç‰©æ–™äº§å“é¡µé¢ä»å¯æ‰‹å·¥è¡¥å¡«åˆ†ç±» |
| MES ç‰©æ–™å‹¾é€‰åŒæ­¥åˆ° MDM | åˆ†ç±»å›žå†™ `mdm_item.category_id` ä¿æŒåŽŸé€»è¾‘ï¼ˆå“ç§ç®¡ç†é¡µé¢ä¸å±•ç¤ºè¯¥å€¼ï¼‰ |
| å“ç§åˆ†ç±»ç®¡ç†æ¨¡å— | ä¸æ”¹åŠ¨ï¼Œåˆ†ç±»æ•°æ®ä»ä¾› MES ç‰©æ–™åˆ†ç±»ç­‰åœºæ™¯ä½¿ç”¨ |
## æ³¨æ„äº‹é¡¹
- é”€å”®ä»·ã€æˆæœ¬ä»·æœªä¸‹çº¿ï¼Œè¡¨å•与列表保持原样。
- åˆ—表删除两列后列宽自行微调。
- å“ç§é€‰æ‹©å¼¹çª—(MdmItemSelect/MdmItemSelectDialog)被采购、库存等多处业务页复用,删除分类列后请回归验证选品带入(编码/名称/单位/条码)是否正常。
docs/mdm_unit_measure_type_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
# MDM è®¡é‡å•位新增「类型」字段 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- åŸºç¡€æ•°æ® > MDM > è®¡é‡å•位(列表、新增/编辑弹窗)
## API
接口路径不变,仅请求/响应体新增 `type` å­—段。
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mdm/unit/create | åˆ›å»ºè®¡é‡å•位(请求体新增 type) |
| PUT | /mdm/unit/update | æ›´æ–°è®¡é‡å•位(请求体新增 type) |
| GET | /mdm/unit/get | èŽ·å¾—è®¡é‡å•ä½ï¼ˆå“åº”æ–°å¢ž type) |
| GET | /mdm/unit/page | èŽ·å¾—è®¡é‡å•ä½åˆ†é¡µï¼ˆå“åº”æ–°å¢ž type) |
| GET | /mdm/unit/list | èŽ·å¾—è®¡é‡å•ä½åˆ—è¡¨ï¼ˆå“åº”æ–°å¢ž type) |
**请求参数(新增):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| type | String | æ˜¯ | ç±»åž‹ï¼Œå•选,可选值固定为:`48千克`、`100千克`、`吨`(三选一,直接存文本;后端 `@NotBlank` æ ¡éªŒï¼‰ |
**响应字段(新增):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| type | String | ç±»åž‹ï¼ŒåŽ†å²æ•°æ®ä¸º null |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "code": "BAG",
    "name": "袋",
    "type": "48千克",
    "status": 0
  }
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹è¯´æ˜Žï¼ˆå·²éšæœ¬æ¬¡åŽç«¯æ”¹åŠ¨ä¸€å¹¶å®Œæˆï¼Œä¾›æ ¸å¯¹ï¼‰
| ä½ç½® | è¦æ±‚ |
|------|------|
| æ–°å¢ž/编辑弹窗表单 | å¢žåŠ ã€Œç±»åž‹ã€ä¸‹æ‹‰é€‰æ‹©ï¼Œé€‰é¡¹å›ºå®šä¸‰é¡¹ï¼š48千克 / 100千克 / å¨ï¼Œå•选、必填 |
| åˆ—表表格 | å¢žåŠ ã€Œç±»åž‹ã€åˆ—å±•ç¤ºï¼Œç©ºå€¼æ˜¾ç¤ºä¸º `-` |
| æŸ¥è¯¢è¡¨å• | æš‚不提供类型筛选(后端分页接口未加该过滤条件) |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| å­˜é‡æ•°æ® | è€è®°å½• `type` ä¸ºç©ºï¼Œåˆ—表展示 `-`;编辑老记录时必须补选类型后才能保存(后端强校验) |
| é€‰é¡¹æ¥æº | é€‰é¡¹ä¸ºå‰ç«¯å†™æ­»ä¸‰é¡¹æ–‡æœ¬ï¼Œä¸å»ºæ•°æ®å­—典;提交的 value å³ä¸­æ–‡æ–‡æœ¬æœ¬èº« |
| åŽç«¯æ ¡éªŒ | `@NotBlank` å¿…填校验;取值范围不限制,前端下拉需保证只会提交三个固定值 |
## æ³¨æ„äº‹é¡¹
- `type` ä¸ºæ–‡æœ¬å­—段(VARCHAR(20)),保存时传中文原文,不要传编码或数字。
- è¯¥å­—段必填,为空时接口返回参数校验错误「类型不能为空」。
- è®¡é‡å•位被物料引用后仍可修改类型,无级联限制。
docs/mes_bag_code_package_refactor_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
# ä¸€è¢‹ä¸€ç ç”Ÿäº§é€»è¾‘改造 - å‰ç«¯è”调方案
> é…å¥—需求方案:`docs/mes_bag_code_package_refactor_requirements.md`。本方案覆盖接口变更与页面数据带入,前端主体改动已随本次改造同步完成,供联调回归使用。
## æ¶‰åŠé¡µé¢
- èµ‹ç ç®¡ç†ï¼ˆè¢‹ç ï¼‰ï¼šç”Ÿæˆæ•°æ®åŒ…弹窗、生成原始袋码弹窗、袋码列表、录入批次、绑定托盘、追溯弹窗
- ç”Ÿäº§æ‰¹æ¬¡ï¼šæ–°å¢ž/编辑/补录表单(新增「绑定托盘」多选)、列表、搜索区
- æ‰˜ç›˜ç®¡ç†ï¼šé¢„生成弹窗、列表、绑定批次弹窗
- å…¬å…± H5 æº¯æºé¡µã€æ‰«ç è¿½æº¯å¼¹çª—(回归)
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. ç”Ÿæˆæ•°æ®åŒ…:选品种 â†’ å‰ç«¯æŒ‰å“ç§ä¸»è®¡é‡å•位类型联动(查 `/mdm/item/get` å– `unitMeasureId`,再查 `/mdm/unit/get?id=` å– `type`):
   - `48千克`/`100千克` â†’ è¾“å…¥**托盘数**,预览「每托 N è¢‹ Ã— æ‰˜ç›˜æ•° = è¢‹ç æ•°ã€
   - `吨` â†’ è¾“å…¥**袋码数**(无托盘)
   - ç±»åž‹æœªé…ç½® â†’ æç¤ºå‰å¾€ã€Œè®¡é‡å•位」维护,提交时后端也会兜底报错
2. ç”Ÿæˆç»“果:同一 uuid è¢‹ç ç»„ + è‡ªåŠ¨ç”Ÿæˆçš„ä¸´æ—¶æ‰˜ç›˜ï¼ˆPL-序号),袋码已带品种、已绑托盘。
3. å½•入批次:勾选袋码 â†’ é€‰ç”Ÿäº§æ‰¹æ¬¡ã€‚后端级联:临时托盘升正式码(批次号-P序号)并绑批次、回写袋码冗余托盘码。混品种会被拦截。
4. æ‰¹æ¬¡ä¸å†å½•å…¥/展示品种;品种信息全部由袋码数据包携带。
5. æ–°å¢ž/补录/编辑批次时均可选「绑定托盘」(多选、非必填,数据源 `GET /mes/pro/pallet/unbound-list`,展示临时托盘码+已装袋数):勾选后批次状态直接为**生产中**,后端同事务复用托盘绑定批次逻辑(托盘升正式码、托盘下袋码级联录入),任一校验失败整体回滚;编辑时仅需勾选本次新增的托盘(已绑定托盘不在下拉中、不可解绑)。
## API
| æ–¹æ³• | è·¯å¾„ | å˜æ›´ |
|------|------|------|
| POST | /mes/pro/bag-code/generate | è¯·æ±‚/响应结构变更(见下) |
| POST | /mes/pro/bag-code/raw-generate | **新增**:生成原始袋码,请求 `{ itemId, count, remark? }`;仅按数量生成、不换算计量单位类型、不生成托盘;响应结构同 generate(`type/perPallet` ä¸ºç©ºã€`palletIds` ç©ºæ•°ç»„) |
| PUT | /mes/pro/bag-code/bind-batch | è·¯å¾„不变;新增品种一致校验、托盘级联绑定;已导出印刷(PRINTED)袋码也允许录入批次 |
| PUT | /mes/pro/pallet/bind-batch | è·¯å¾„不变;托盘绑定批次时,托盘下未绑批次的袋码自动级联录入同一批次(品种一致校验、回写批次袋数) |
| DELETE | /mes/pro/bag-code/delete-by-uuid | è·¯å¾„不变;级联删除本组自动生成托盘 |
| POST | /mes/pro/bag-code/replace | ä¸å˜ï¼›æ–°è¢‹ç è‡ªåŠ¨ç»§æ‰¿åŽŸå“ç§ |
| GET | /mes/pro/bag-code/page、list-by-uuid、get-by-code | å“åº”新增品种字段;page æ–°å¢žå¯é€‰æŸ¥è¯¢å‚æ•° `itemId`(品种,等值匹配;导出 Excel åŒå£å¾„) |
| POST | /mes/pro/pallet/batch-create、pre-generate | è¯·æ±‚去掉 capacity |
| GET | /mes/pro/pallet/page | å“åº”去掉 capacity,保留 autoGenerated |
| POST/PUT | /mes/pro/batch/create、update、fill-info | è¯·æ±‚去掉 itemId;三者均新增可选 `palletIds`(绑定时批次进入生产中) |
| GET | /mes/pro/pallet/unbound-list | **新增**:未绑定批次的临时托盘列表(权限 mes:pro-pallet:query),供批次新增/补录绑定托盘下拉 |
| GET | /mes/pro/batch/page、get、simple-list | å“åº”去掉 itemId/itemCode/itemName/itemSpecification |
**生成数据包请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| itemId | Long | æ˜¯ | å“ç§ç¼–号 |
| palletCount | Integer | æ¡ä»¶å¿…å¡« | æ‰˜ç›˜æ•°ï¼ˆç±»åž‹=N千克) |
| bagCount | Integer | æ¡ä»¶å¿…å¡« | è¢‹ç æ•°ï¼ˆç±»åž‹=吨) |
| remark | String | å¦ | å¤‡æ³¨ |
**生成数据包响应示例:**
```json
{
  "code": 0,
  "data": {
    "uuid": "生成组UUID",
    "type": "48千克",
    "perPallet": 48,
    "totalCount": 96,
    "palletIds": [101, 102]
  }
}
```
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å“ç§ç¼–码/名称(itemCode/itemName) | è¢‹ç åˆ—表、追溯、导出 Excel | ç”Ÿæˆæ—¶ç»‘定在袋码上 |
| autoGenerated | æ‰˜ç›˜åˆ—表「自动生成」Tag | æ•°æ®åŒ…自动创建的托盘标识 |
| å“ç§ï¼ˆæ‰¹æ¬¡é¡µï¼‰ | ä¸å†å±•示 | è¡¨å•/列表/搜索均已移除 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| ç±»åž‹=N千克 ç”Ÿæˆ | è¢‹ç æ•° = N Ã— æ‰˜ç›˜æ•°ï¼Œè‡ªåŠ¨å»º N ä¸ªæ‰˜ç›˜å¹¶å‡åˆ†ç»‘定 |
| ç±»åž‹=吨 ç”Ÿæˆ | æ— æ‰˜ç›˜ï¼Œè¢‹ç ä»…绑批次 |
| ç”ŸæˆåŽŸå§‹è¢‹ç  | ä»…需品种+数量;袋码带品种、不绑批次/托盘,后续录入批次、绑托盘、导出印刷、删除组与数据包袋码完全一致;不校验品种计量单位类型 |
| å½•入批次混品种 | æŠ¥é”™ã€Œå½•入批次的袋码必须属于同一品种」(与批次已有袋码一并校验) |
| æ‰˜ç›˜ç»‘定批次 | æ‰˜ç›˜ä¸‹æœªç»‘批次的袋码(已生成/已导出印刷)自动随托盘录入同一批次,袋码冗余托盘码同步更新为正式码 |
| æ‰¹æ¬¡æ–°å¢ž/补录/编辑绑定托盘 | palletIds éžç©ºæ—¶æ‰¹æ¬¡çŠ¶æ€ç›´æŽ¥ç½®ã€Œç”Ÿäº§ä¸­ã€ï¼ŒåŒäº‹åŠ¡å¤ç”¨æ‰˜ç›˜ç»‘å®šæ‰¹æ¬¡é€»è¾‘ï¼ˆæ­£å¼ç +袋码级联+品种一致校验),失败整体回滚;新增/补录未勾选则维持草稿;编辑取消勾选不回退状态、已绑定托盘不可解绑 |
| ç»‘托盘容量 | æ— ä¸Šé™ï¼Œè‡ªåŠ¨æ‰˜ç›˜å¯ç»§ç»­æ‰‹åŠ¨ç»‘å…¥ |
| åˆ é™¤æ•°æ®åŒ… | ç»„内托盘已绑批次或有正式码则不可删,否则级联删自动托盘 |
| è¿½æº¯å“ç§ | ä¼˜å…ˆè¢‹ç å“ç§ï¼›åŽ†å²è¢‹ç å›žé€€æ‰¹æ¬¡å“ç§ï¼ˆå…¬å…± H5 åŒæ­¥æ”¯æŒï¼‰ |
| æ‰¹æ¬¡å· | PC + yyyyMMdd + äº§çº¿ç¼–码 + æµæ°´å·ï¼ˆä¸å†å«å“ç§ç¼–码) |
## æ³¨æ„äº‹é¡¹
- `generate` æ—§å‚æ•° `count` å·²åºŸå¼ƒï¼Œåªä¼  `palletCount` æˆ– `bagCount`(按品种类型二选一)。
- æ‰¹æ¬¡ `itemId` ç›¸å…³å­—段已从接口移除,勿再引用(批次追溯视图 `get-by-code` çš„品种字段保留,可能为空=新批次)。
- æ‰˜ç›˜è¿½æº¯è§†å›¾ï¼ˆPalletTrace/qr-trace-modal)仍返回 `capacity` ä¾›åŽ†å²æ•°æ®å±•ç¤ºï¼Œæ–°æ‰˜ç›˜ä¸ºç©ºã€‚
- è€æ•°æ®ï¼ˆåŽ†å²ç©ºç™½è¢‹ç ï¼‰å“ç§ä¸ºç©ºå±žæ­£å¸¸ï¼Œè¿½æº¯èµ°æ‰¹æ¬¡å›žé€€ã€‚
- `palletIds` åœ¨ create/update/fill-info å‡ç”Ÿæ•ˆï¼›ä¸‹æ‹‰æ¯æ¬¡æ‰“开弹窗重新拉取 `unbound-list`,若提交时所选托盘已被他人绑定,后端整体报错回滚(本次批次操作不生效),提示用户刷新重试。
- è¢‹ç åˆ†é¡µï¼ˆ`/page`)后端已改为**创建时间倒序**(同秒内按编号倒序),前端无需再传排序参数;导出 Excel åŒå£å¾„。
- åŽŸå§‹è¢‹ç ä¸Žæ•°æ®åŒ…è¢‹ç åŒè¡¨åŒè§„åˆ™ï¼ˆuuid æˆç»„、可整组删除),列表页通过生成组 UUID åŒºåˆ†æ¥æºã€‚
- åˆ—表搜索区新增「品种」条件(复用 `MdmItemSelect`,可清空),传 `itemId` ç­‰å€¼è¿‡æ»¤ï¼›å¯¼å‡º Excel æºå¸¦åŒä¸€æŸ¥è¯¢æ¡ä»¶åŒå£å¾„生效。历史空白袋码品种为空,按品种筛选不会命中。
docs/mes_bag_code_package_refactor_requirements.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
# ä¸€è¢‹ä¸€ç ç”Ÿäº§é€»è¾‘改造 - éœ€æ±‚方案
> çŠ¶æ€ï¼šå·²ç¡®è®¤å¹¶å®Œæˆå®žæ–½ï¼ˆ2026-09-17)。决策点定稿见文末「6. ç¡®è®¤ç»“论」。
## 1. éœ€æ±‚背景
一袋一码的生产逻辑调整:
- æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†**不再绑定品种**,品种下沉到「赋码管理」的数据包上。
- ç”Ÿæˆæ•°æ®åŒ…时选择**品种**并输入**托盘数**,系统按品种主计量单位的「类型」自动计算袋码数量并生成袋码。
- æŒ‰æ‰˜ç›˜æ•°**自动生成托盘并自动绑定**袋码;托盘不再需要「可装袋数」。
- å“ç§ä¸ºã€Œå¨ã€æ—¶æ— æ‰˜ç›˜ï¼šæ”¹ä¸ºç›´æŽ¥è¾“å…¥**袋码数**,袋码只绑生产批次、不绑托盘码。
## 2. çŽ°çŠ¶è¦ç‚¹
| å¯¹è±¡ | çŽ°çŠ¶ |
|------|------|
| æ•°æ®åŒ…生成 | `POST /mes/pro/bag-code/generate` ä»…ä¼  `count`,生成空白袋码(无品种/批次/托盘) |
| æ‰¹æ¬¡ | `mes_pro_batch.itemId` ç»‘定品种;批次号 = PC + æ—¥æœŸ + äº§çº¿ç¼–码 + å“ç§ç¼–码 + æµæ°´å· |
| æ‰˜ç›˜ | æ‰‹åŠ¨åˆ›å»ºï¼ˆæ‰¹é‡ç”Ÿæˆ/预生成),创建时填 `capacity å¯è£…袋数`;袋码手动绑托盘并校验容量 |
| å“ç§â†’单位 | `mdm_item.unitMeasureId â†’ mdm_unit_measure.type`(类型选项:48千克/100千克/吨,本次已加) |
| è¿½æº¯ | æ‰«ç è¿½æº¯çš„品种信息取自批次 `batch.itemId` |
## 3. æ”¹é€ æ–¹æ¡ˆ
### 3.1 ç”Ÿæˆæ•°æ®åŒ…(赋码管理,核心改动)
**换算规则**(后端计算,不信任前端传值):
| å“ç§ä¸»å•位类型 | è¾“å…¥ | ç”Ÿæˆç»“æžœ |
|------|------|------|
| 48千克 | æ‰˜ç›˜æ•° P | è¢‹ç  48 Ã— P ä¸ªï¼›è‡ªåŠ¨ç”Ÿæˆ P ä¸ªæ‰˜ç›˜ï¼Œæ¯æ‰˜ç»‘定 48 ä¸ªè¢‹ç  |
| 100千克 | æ‰˜ç›˜æ•° P | è¢‹ç  100 Ã— P ä¸ªï¼›è‡ªåŠ¨ç”Ÿæˆ P ä¸ªæ‰˜ç›˜ï¼Œæ¯æ‰˜ç»‘定 100 ä¸ªè¢‹ç  |
| å¨ | è¢‹ç æ•° N | è¢‹ç  N ä¸ªï¼›ä¸ç”Ÿæˆæ‰˜ç›˜ï¼Œè¢‹ç ä¸ç»‘托盘 |
| ç±»åž‹ä¸ºç©º/无法解析 | - | æŠ¥é”™ï¼šå“ç§ä¸»è®¡é‡å•位未配置类型(48千克/100千克/吨) |
**接口变更** `POST /mes/pro/bag-code/generate`:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| itemId | Long | æ˜¯ | å“ç§ï¼ˆmdm_item ç¼–号) |
| palletCount | Integer | æ¡ä»¶å¿…å¡« | æ‰˜ç›˜æ•°ï¼ˆç±»åž‹ä¸º N千克 æ—¶å¿…填) |
| bagCount | Integer | æ¡ä»¶å¿…å¡« | è¢‹ç æ•°ï¼ˆç±»åž‹ä¸º å¨ æ—¶å¿…填) |
| remark | String | å¦ | å¤‡æ³¨ |
响应新增:`uuid、type、perPallet(每托袋数)、totalCount(生成袋码数)、palletIds(自动生成的托盘)`。
**事务逻辑**:一次生成 = åŒä¸€ uuid çš„袋码组 + åŒæ‰¹è‡ªåŠ¨ç”Ÿæˆçš„æ‰˜ç›˜ï¼›æ‰˜ç›˜å…ˆä»¥**临时码(PL-序号)**落库并完成袋码绑定,正式托盘码在「录入批次」时生成。
**生成数据包弹窗(前端)**:选择品种(沿用品种选择组件)→ è¯»å–该品种主单位类型(前端查 `mdm_unit_measure`,类型为空给提示)→ æŒ‰ç±»åž‹åˆ‡æ¢è¾“入「托盘数」或「袋码数」→ å®žæ—¶é¢„览"将生成 è¢‹ç  X ä¸ª / æ‰˜ç›˜ Y ä¸ª"。
### 3.2 å½•入批次(保持后录入,扩展级联)
`PUT /mes/pro/bag-code/bind-batch` æµç¨‹ä¸å˜ï¼Œæ–°å¢žçº§è”:
1. è¢‹ç ç»‘定批次(现状)。
2. è¯¥ç»„袋码所绑定的临时托盘自动绑定同一批次,并分配正式托盘码 `批次号-P序号`,回写批次托盘数。
3. æ–°å¢žæ ¡éªŒï¼šåŒä¸€æ•°æ®åŒ…(同批绑定袋码)品种必须一致(生成时天然一致,防手动混选)。
### 3.3 è¢‹ç æ•°æ®æ¨¡åž‹
`mes_pro_bag_code` æ–°å¢žï¼š`item_id`、`item_code`、`item_name`(冗余)。
- åˆ—表/查询:显示品种列(编码/名称可检索本期不做,仅展示)。
- åˆ é™¤æ•°æ®åŒ…(delete-by-uuid):级联删除本组**自动生成**的托盘(前提仍是全部未绑批次;已录批次维持现状不可删)。
- è¢‹ç ä¸å¼ºåˆ¶æ‰˜ç›˜ï¼š`pallet_id` ä¿æŒå¯ç©ºï¼ˆå¨ç±»åž‹ï¼‰ã€‚
### 3.4 æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†
- **去掉品种字段**:创建/编辑表单、列表列、查询条件、Save/Resp/Page VO å‡ç§»é™¤ `itemId`;DO ä¸Žæ•°æ®åº“列 `mes_pro_batch.item_id` ä¿ç•™ï¼ˆåŽ†å²æ•°æ®ã€è¿½æº¯å›žé€€ç”¨ï¼‰ï¼Œæ³¨é‡Šæ ‡è®°"已废弃"。
- **批次号规则改为** `PC + yyyyMMdd + äº§çº¿ç¼–码 + æµæ°´å·`(`generateBatchCode` ä¸å†æ‹¼å“ç§ç¼–码;自动编码规则配置无需改)。
- ç¼–辑批次不再因品种变化重新生成批次号。
### 3.5 æ‰˜ç›˜ç®¡ç†
- æ‰¹é‡ç”Ÿæˆ/预生成接口去掉 `capacity` å…¥å‚;表单去掉「可装袋数」字段;列表去掉该列。DB åˆ—保留兼容历史。
- è¢‹ç æ‰‹åŠ¨ç»‘æ‰˜ç›˜åŽ»æŽ‰å®¹é‡æ ¡éªŒï¼ˆ`capacity` åˆ¤ç©ºé€»è¾‘天然跳过,删除相关校验代码)。
- æ‰˜ç›˜åˆ—表为自动生成托盘打标识(复用 `auto_generated` å­—段 = true)。
### 3.6 è¿½æº¯/扫码/看板(回归项)
- è¢‹ç è¿½æº¯ï¼ˆ`/get-by-code`、TraceRespVO):品种改为**优先取袋码 item_id,历史袋码回退批次 itemId**。
- æ‰¹æ¬¡è¿½æº¯è§†å›¾ï¼ˆMesProBatchTraceRespVO):保留品种返回但可能为空(历史批次有值)。
- æ‰«ç å…¥åº“、销售出库、袋码首页看板:不直接依赖批次品种的逻辑不动,涉及处回归验证。
## 4. æ•°æ®åº“变更(mom-thzy)
```sql
ALTER TABLE mes_pro_bag_code ADD COLUMN item_id BIGINT NULL COMMENT '品种(物料)编号' AFTER pallet_code;
ALTER TABLE mes_pro_bag_code ADD COLUMN item_code VARCHAR(64) NULL COMMENT '品种编码(冗余)' AFTER item_id;
ALTER TABLE mes_pro_bag_code ADD COLUMN item_name VARCHAR(255) NULL COMMENT '品种名称(冗余)' AFTER item_code;
-- mes_pro_batch.item_id ä¿ç•™ä¸åˆ ï¼ŒJava æ³¨é‡Šæ ‡è®°åºŸå¼ƒ
```
## 5. å‰åŽç«¯æ”¹åŠ¨æ¸…å•
| å±‚ | æ–‡ä»¶ | æ”¹åЍ |
|----|------|------|
| DB | mes_pro_bag_code | +item_id/item_code/item_name |
| åŽç«¯ | MesProBagCodeGenerateReqVO/RespVO、ServiceImpl | æ–°å‚数、类型换算、自动建托盘、事务 |
| åŽç«¯ | MesProBagCodeDO/RespVO/Mapper | +品种字段 |
| åŽç«¯ | bind-batch æµç¨‹ | çº§è”托盘绑批次、品种一致性校验 |
| åŽç«¯ | MesProBatchServiceImpl/VO | åŽ»å“ç§ã€æ‰¹æ¬¡å·è§„åˆ™ã€ç¼–è¾‘é€»è¾‘ |
| åŽç«¯ | MesProPalletServiceImpl/VO | åŽ» capacity å…¥å‚与校验 |
| åŽç«¯ | delete-by-uuid | çº§è”删自动托盘 |
| åŽç«¯ | è¿½æº¯ VO | å“ç§ä¼˜å…ˆè¢‹ç ã€å›žé€€æ‰¹æ¬¡ |
| å‰ç«¯ | pro/bagCode generate-form | å“ç§+托盘数/袋码数表单 |
| å‰ç«¯ | pro/bagCode åˆ—表 | +品种列 |
| å‰ç«¯ | pro/batch form/list/search | ç§»é™¤å“ç§ |
| å‰ç«¯ | pro/pallet form/list | ç§»é™¤å¯è£…袋数 |
## 6. ç¡®è®¤ç»“论(定稿)
1. **混品种**:要。一次 bind-batch å‹¾é€‰çš„袋码品种必须一致,否则报错「录入批次的袋码必须属于同一品种」(历史空白袋码不参与校验)。
2. **自动托盘可否再手动增绑**:无容量上限,装满后仍可继续手动绑入袋码。
3. **批次列表历史数据**:老批次 itemId ç›´æŽ¥ä¸å±•示即可,无数据迁移;追溯视图保留批次品种做历史回退。
4. **流程与批次号**:保持「先生成数据包、后录入批次」;批次号规则改为 `PC + yyyyMMdd + äº§çº¿ç¼–码 + æµæ°´å·`。
## 7. å®žæ–½è®°å½•(2026-09-17)
- DB(mom-thzy):`mes_pro_bag_code` æ–°å¢ž `item_id/item_code/item_name`;`mdm_unit_measure.type`(48千克/100千克/吨);`mes_pro_batch.item_id` ä¿ç•™æ ‡åºŸå¼ƒã€‚
- åŽç«¯ï¼š`generate` é‡å†™ï¼ˆç±»åž‹æ¢ç®—+自动临时托盘)、`bind-batch` çº§è”托盘正式码+品种一致校验、`delete-by-uuid` çº§è”删自动托盘、`replace` ç»§æ‰¿å“ç§ã€æ‰˜ç›˜åˆ›å»ºåŽ» `capacity` å…¥å‚、袋码绑托盘去容量校验、批次 VO/Service åŽ»å“ç§ã€æ‰¹æ¬¡å·å•äº§çº¿è¦ç´ ã€è¢‹ç /公共追溯与二维码标签品种「袋码优先、批次回退」。
- å‰ç«¯ï¼šè¢‹ç ç”Ÿæˆå¼¹çª—(品种+托盘数/袋码数动态+预览)、列表品种列;批次表单/列表/搜索去品种;托盘表单/列表去可装袋数、列表加「自动生成」标识、绑托盘下拉去容量提示。
- ç¼–译:`mvn compile -pl yudao-module-mes -am -q` é€šè¿‡ã€‚
## 8. è¡¥å……需求:批次操作绑定托盘(2026-09-17)
- æ‰¹æ¬¡æ–°å¢ž/补录/编辑均可选「绑定托盘」(多选、非必填):勾选后批次状态直接置**生产中**,同事务复用托盘绑定批次逻辑(正式托盘码、托盘下袋码级联录入、品种一致校验),任一步失败整体回滚。
- ç¼–辑时仅传本次新增勾选的托盘;已绑定托盘不在下拉中(unbound-list å·²è¿‡æ»¤ï¼‰ï¼Œä¸æ”¯æŒè§£ç»‘,未勾选不改状态、不截断回退。
- æ–°æŽ¥å£ `GET /mes/pro/pallet/unbound-list`:batch_id ä¸ºç©ºã€æ— æ­£å¼æ‰˜ç›˜ç ä¸”未作废的临时托盘。
- å‰ç«¯è”调细则见 `docs/mes_bag_code_package_refactor_integration.md`。
## 9. è¡¥å……需求:原始袋码与列表排序(2026-09-17)
- æ–°å¢ž `POST /mes/pro/bag-code/raw-generate`:仅选品种+袋码数量即生成对应数量袋码(带品种冗余、uuid æˆç»„),**不校验计量单位类型、不生成托盘**;后续录入批次/绑托盘/印刷/删除与数据包袋码一致。赋码管理页工具栏加「生成原始袋码」弹窗入口。
- è¢‹ç åˆ†é¡µæŸ¥è¯¢æ”¹ä¸ºæŒ‰**创建时间倒序**(同秒按编号倒序),原「袋序号升序」废弃。
- ç¼–译 `mvn compile -pl yudao-module-mes -am -q` é€šè¿‡ã€å‰ç«¯ç±»åž‹æ£€æŸ¥æ”¹åŠ¨æ–‡ä»¶é›¶é”™è¯¯ã€‚
## 10. è¡¥å……需求:袋码列表品种查询(2026-09-17)
- èµ‹ç ç®¡ç†åˆ—表搜索区新增「品种」条件(复用 `MdmItemSelect` é€‰æ‹©ç»„件),按 `mes_pro_bag_code.item_id` ç­‰å€¼è¿‡æ»¤ï¼›åˆ†é¡µä¸Žå¯¼å‡º Excel åŒå£å¾„生效。
- åŽ†å²ç©ºç™½è¢‹ç  `item_id` ä¸ºç©ºï¼Œé€‰æ‹©å“ç§åŽä¸ä¼šå‘½ä¸­ï¼ˆå±žæ­£å¸¸ï¼‰ã€‚§3.3 åŽŸã€Œç¼–ç /名称可检索本期不做,仅展示」就此升级为按品种编号检索。
docs/mes_pro_batch_processing_team_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
# æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†ï¼šéšè—äº§çº¿ç¼–码、字段改名、新增加工班组 - å‰ç«¯è”调方案
## èƒŒæ™¯
批次生产管理页面(/mes/pro/batch)调整:
- éšè—ã€Œäº§çº¿ç¼–码」(列表列、搜索项、Excel å¯¼å‡ºå‡ä¸å†å±•示;接口仍返回 `lineCode`)。
- ã€Œäº§çº¿åç§°ã€å±•示文案统一改为「加工单位」(数据仍为 `lineName`,来源产线主数据)。
- ã€Œè®¡åˆ’数量」展示文案改为「加工数量」(数据仍为 `planQuantity`;2026-09-17 ç”±ã€Œä»·æ ¼æ•°é‡ã€å†æ¬¡æ›´åï¼‰ã€‚
- æ–°å¢žã€ŒåŠ å·¥ç­ç»„ã€å­—æ®µï¼Œæ‰‹å·¥å¡«å†™æ–‡æœ¬ï¼Œæœ€é•¿ 64 å­—符,位置在「加工单位」之后。
## API å˜åŒ–
| æ–¹æ³• | è·¯å¾„ | å˜åŒ– |
|------|------|------|
| POST | /mes/pro/batch/create | è¯·æ±‚体新增 `processingTeam`(可选) |
| PUT | /mes/pro/batch/update | åŒä¸Š |
| PUT | /mes/pro/batch/fill-info | åŒä¸Šï¼ˆä¸´æ—¶æ‰¹æ¬¡è¡¥å½•) |
| GET | /mes/pro/batch/get | å“åº”新增 `processingTeam` |
| GET | /mes/pro/batch/page | å“åº”新增 `processingTeam`;新增查询参数 `processingTeam`(模糊匹配) |
**字段说明:**
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| processingTeam | String(64) | å¦ | åŠ å·¥ç­ç»„ï¼Œæ‰‹å·¥å¡«å†™ï¼Œä¸é€‰äº§çº¿ä¹Ÿå¯å¡« |
**响应示例(节选):**
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "code": "PC20260917L01NY001",
        "lineName": "包装一线",
        "processingTeam": "一号加工班组",
        "planQuantity": 1000,
        "ownerUserName": "张三"
      }
    ],
    "total": 1
  }
}
```
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ–°å¢ž/补录/编辑批次 | ã€ŒåŠ å·¥ç­ç»„ã€ä¸ºå¯é€‰æ–‡æœ¬è¾“å…¥ï¼›ç¼–è¾‘æ—¶ä¸ä¼ ä¸ä¼šæ¸…ç©ºåŽ†å²å€¼ |
| åŠ å·¥å•ä½ | ä»ç”±é€‰æ‹©äº§çº¿ï¼ˆ`lineId`)回填 `lineName`,仅展示文案变化 |
| åŠ å·¥æ•°é‡ | ä»…展示文案变化,字段与校验逻辑不变 |
| äº§çº¿ç¼–码 | æŽ¥å£ä¿ç•™ `lineCode`(扫码、批次号生成等仍依赖),页面与导出隐藏 |
| å¯¼å‡º Excel | åˆ—同步变化:去掉「产线编码」,「产线名称→加工单位」,新增「加工班组」,「计划数量→加工数量」 |
## æ•°æ®åº“变更
`mes_pro_batch` æ–°å¢žåˆ— `processing_team VARCHAR(64) NULL COMMENT '加工班组'`(脚本:`docs/sql/pro_batch_processing_team.sql`)。
## æ³¨æ„äº‹é¡¹
- æ‰«ç è¿½æº¯å¼¹çª—(qr-trace-modal)、批次追溯页(qc/batchtrace)本次未要求调整,仍展示产线编码/名称与计划数量文案,如需统一另行处理。
- æœ¬æ‰¹åŽç«¯å·²åŒæ­¥ä¿®æ”¹å‰ç«¯ `src/views/mes/pro/batch/`(列、搜索表单、编辑表单)与 `src/api/mes/pro/batch/index.ts` ç±»åž‹ï¼Œä¾›å‰ç«¯å›¢é˜Ÿå¤æ ¸ã€‚
docs/mes_wm_material_stock_bag_api.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
# åº“存袋码明细联调方案
## ä¸šåŠ¡æµç¨‹
种子质检以袋码提交。质检合格时,暂存库存汇总数量减少 1,合格库存汇总数量增加 1,同时该袋码明细迁移到合格仓;质检不合格时,暂存库存汇总数量减少 1,袋码明细标记为报废。其他袋码不受影响。
库存台账继续按物料、批次、仓库、库区、库位和供应商汇总;袋码明细用于记录每个袋码的具体状态和位置。
## è¢‹ç æ˜Žç»†ç”Ÿå‘½å‘¨æœŸï¼ˆç»Ÿä¸€æœåŠ¡æ‰˜ç®¡ï¼‰
所有带袋码的库存变动统一通过库存事务服务在同一事务内同步维护 **汇总库存 + è¢‹ç æ˜Žç»† + åº“存流水** ä¸‰è€…一致,各业务入口无需各自实现袋码逻辑。生命周期:
| é˜¶æ®µ | åŠ¨ä½œ | è¢‹ç æ˜Žç»†å˜åŒ– |
|------|------|--------------|
| å•袋扫码入库 | ç”Ÿäº§æ‰«ç å…¥åº“(扫码类型 BAG) | å»ºç«‹æ´»åŠ¨æ˜Žç»†ï¼ˆæš‚å­˜åº“ï¼‰ |
| ç§å­è´¨æ£€åˆæ ¼ | æš‚存出库 + åˆæ ¼å…¥åº“ | æ˜Žç»†ç”±æš‚存库迁移到合格库 |
| ç§å­è´¨æ£€ä¸åˆæ ¼ | æš‚存出库 | æ˜Žç»†æŠ¥åºŸ |
| é”€å”®å‡ºåº“完成 | å‡ºåº“扣减 | æ˜Žç»†ç½®ä¸ºå·²å‡ºåº“ |
| æ•´ä½ç§»åº“ | è°ƒæ‹¨ç§»å‡º + ç§»å…¥ | æ˜Žç»†ä½ç½®è¿ç§»åˆ°ç›®æ ‡åº“位 |
说明:
- æ‰˜ç›˜ï¼ˆPALLET)/批次(BATCH)整批入库、采购/杂项/退货等按行按数量入库的出入口,为批量汇总,不逐袋建立明细;这些袋码在首次种子质检时通过按批次定位暂存库存补建明细。
- äº‹åŠ¡æµæ°´ DTO å¢žåŠ  `bagCodeId`/`bagCode` å­—段;出库/移入按袋码自动更新袋码明细。
- æ¯ç¬”带袋码的入库、出库、移库、质检都会生成对应的 `mes_wm_transaction` æµæ°´å¹¶å›žå¡«è¢‹ç ï¼Œä¾¿äºŽåœ¨åº“存流水页面按袋码筛选追溯。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/mes/wm/material-stock/page` | åº“存汇总分页,响应增加袋码统计字段 |
| GET | `/mes/wm/material-stock/bag-details` | æŸ¥è¯¢æŒ‡å®šåº“存汇总记录下的在库袋码明细 |
| GET | `/mes/wm/transaction/page` | åº“存事务流水,支持 `bagCode` ç­›é€‰å¹¶è¿”回袋码 |
### è¢‹ç æ˜Žç»†æŸ¥è¯¢
请求参数:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| materialStockId | Long | æ˜¯ | åº“存汇总记录 ID |
| bagCode | String | å¦ | è¢‹ç æ¨¡ç³ŠæŸ¥è¯¢ |
| pageNo | Integer | æ˜¯ | é¡µç  |
| pageSize | Integer | æ˜¯ | æ¯é¡µæ¡æ•° |
响应字段包括 `bagCode`、`batchCode`、`stockState`、`status`、`quantity`、`warehouseId`、`locationId`、`areaId`、`inboundTime` å’Œ `outboundTime`。
库存汇总分页响应新增:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| bagCount | Long | å½“前在库袋码明细数量 |
| bagDetailSupported | Boolean | æ˜¯å¦å­˜åœ¨è¢‹ç æ˜Žç»†ï¼›åŽ†å²æ— æ³•è¿˜åŽŸè¢‹ç çš„åº“å­˜ä¸º false |
## å‰ç«¯è°ƒç”¨æ—¶åº
1. è°ƒç”¨åº“存台账分页接口获取汇总记录。
2. å¯¹ `bagDetailSupported=true` çš„记录显示“查看袋码”。
3. ç‚¹å‡»åŽè°ƒç”¨ `/mes/wm/material-stock/bag-details`,传入当前行 `id` ä½œä¸º `materialStockId`。
4. è¢‹ç æ˜Žç»†ä¸­çš„ `bagCode` å¯ç»§ç»­è°ƒç”¨æ‰¹æ¬¡/袋码追溯接口查看质检和生产信息。
5. åº“存流水页面使用 `bagCode` ç­›é€‰æ—¶ï¼Œå±•示质检迁移产生的袋码级流水。
## ä¸šåŠ¡è§„åˆ™
- åŒä¸€è¢‹ç åªèƒ½å­˜åœ¨ä¸€æ¡åº“存袋码明细。
- è´¨æ£€æŒ‰å®žé™…袋码定位暂存库存,不按批次随机扣减。
- æŠ¥åºŸè¢‹ç ä¸è®¡å…¥åœ¨åº“明细查询,但保留明细记录用于追溯。
- æ— æ³•从历史汇总库存还原袋码时,不伪造袋码明细。
- SQL è„šæœ¬ä¸åŒ…å« `tenant_id` å­—段。
docs/sales_notice_to_product_sales.md
@@ -101,6 +101,20 @@
**注意:** æ‹£è´§æ˜Žç»†éœ€è¦æ‰‹åŠ¨æ·»åŠ 
## å‘货通知行物料校验
新增或修改发货通知行时,物料必须关联批次,且该物料批次必须存在正式库库存。正式库指库存状态为合格库(`stockState = 20`)的库存记录。
| æ ¡éªŒé¡¹ | è§„则 |
|--------|------|
| ç‰©æ–™ | å¿…须存在且启用 |
| æ‰¹æ¬¡ | `batchId` æˆ– `batchCode` å¿…须能匹配有效批次,且批次属于所选物料 |
| æ­£å¼åº“å­˜ | ä»…统计合格库库存,不统计暂存库库存 |
| æ•°é‡ | åŒä¸€ç‰©æ–™ã€æ‰¹æ¬¡çš„合格库库存合计必须不少于发货数量 |
| åº“存变更 | ä¿å­˜å‘货通知行时不扣减、不预占库存,实际出库时再次校验 |
库存不足时,前端应直接展示后端返回的“库存数量不足”业务提示,并保留用户填写的表单内容。
## å­—段映射表
### ä¸»è¡¨å­—段映射
docs/scan_in_multi_type_inbound.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
# ç”Ÿäº§çº¿æ‰«ç å…¥åº“:支持袋码/托盘码/批次码 + åŽ»æŽ‰è®¾å¤‡ç¼–ç  - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§çº¿æ‰«ç å…¥åº“(`src/views/mes/pro/scan-in/index.vue`)
## ä¸šåŠ¡å˜æ›´
1. **去掉设备编码**:扫码入库不再录入/展示设备编码,接口 `deviceCode` æ”¹ä¸ºéžå¿…填,前端不再传。
2. **产线、库区、库位改为下拉选择**:数据源为现有精简列表接口,不再手填编号。
3. **等待扫码区增加"扫码类型"选择**:袋码 / æ‰˜ç›˜ç  / æ‰¹æ¬¡ç ä¸‰é€‰ä¸€ï¼Œæ‰«ç æ¡†éšç±»åž‹æäº¤å¯¹åº”码值。
## API
### ä¸»æŽ¥å£ï¼šè¿½æº¯ç æ‰«ç å…¥åº“
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mes/pro/scan-in/inbound | è¿½æº¯ç æ‰«ç å…¥åº“(袋码/托盘码/批次码) |
**请求参数(本次变更):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | å˜æ›´ | è¯´æ˜Ž |
|------|------|------|------|------|
| scanType | String | å¦ | æ–°å¢ž | æ‰«ç ç±»åž‹ï¼š`BAG` è¢‹ç  / `PALLET` æ‰˜ç›˜ç  / `BATCH` æ‰¹æ¬¡ç ï¼Œä¸ä¼ é»˜è®¤ `BAG` |
| deviceCode | String | å¦ | æ”¹ä¸ºéžå¿…å¡« | è®¾å¤‡ç¼–码,前端不再传 |
| bagCode | String | æ˜¯ | è¯­ä¹‰æ‰©å±• | æ‰«ç å†…容:袋码/托盘码/批次码的码值(含临时码) |
| eventId | String | æ˜¯ | - | äº‹ä»¶å·ï¼ˆå»ºè®®ç”¨ `{scanType}-{时间戳}` ä¿è¯å”¯ä¸€ï¼‰ |
| quantity | Number | æ˜¯ | - | è¢‹ç åœºæ™¯ä¼  1;托盘/批次场景后端忽略该值,按已装袋数/批次袋数计算 |
| lineCode | String | å¦ | - | äº§çº¿ç¼–码(前端下拉选择) |
| locationId | Long | æ˜¯ | - | WIP æš‚存库区编号(前端下拉选择) |
| areaId | Long | æ˜¯ | - | WIP æš‚存库位编号(前端下拉选择) |
**响应新增字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| scanType | String | æœ¬æ¬¡æ‰«ç ç±»åž‹ï¼ˆBAG/PALLET/BATCH) |
**入库数量口径:**
| scanType | æ•°é‡ | è¯´æ˜Ž |
|----------|------|------|
| BAG | å‰ç«¯ä¼  1 | å•袋入库 |
| PALLET | æ‰˜ç›˜å·²è£…袋数 `bag_count` | æ•´æ‰˜ä¸€æ¬¡æ€§å…¥æš‚存库 |
| BATCH | æ‰¹æ¬¡è¢‹æ•° `bag_quantity` | æ•´æ‰¹ä¸€æ¬¡æ€§å…¥æš‚存库 |
**入库去重:** è¢‹ç ã€æ‰˜ç›˜ç ã€æ‰¹æ¬¡ç å‡ä»¥å„自 `inbound_event_id` åˆ¤æ–­æ˜¯å¦å·²å…¥åº“,重复扫码返回对应"已入库"错误码,不会重复写入库存事务。
### ä¸‹æ‹‰æ•°æ®æºï¼ˆå¤ç”¨çŽ°æœ‰æŽ¥å£ï¼Œæ— æ–°å¢žï¼‰
| ç”¨é€” | æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|------|
| äº§çº¿ | GET | /mes/pro/line/simple-list | è¿”回启用产线 `{id, code, name}` |
| åº“区 | GET | /mes/wm/warehouse-location/simple-list | è¿”回库区 `{id, code, name}`;选库区后联动加载其下库位 |
| åº“位 | GET | /mes/wm/warehouse-area/simple-list?locationId= | è¿”回库位 `{id, code, name}` |
## æ•°æ®å¸¦å…¥è¯´æ˜Ž
- äº§çº¿/库区/库位:下拉选择,页面 `onMounted` ä¸€æ¬¡æ€§åŠ è½½äº§çº¿ã€åº“åŒºï¼›é€‰æ‹©åº“åŒºåŽæŒ‰ `locationId` è”动加载库位。
- æ‰«ç æäº¤ï¼š`locationId`、`areaId`、`scanType`、`bagCode` ä¸€å¹¶å¸¦å…¥å…¥åº“接口;产线 `lineCode` æœ‰å€¼æ‰ä¼ ã€‚
- äº‹ä»¶åˆ—表/看板:以 `scanType` åŒºåˆ†è¿½æº¯ç ç±»åž‹å±•示(历史无类型的数据默认显示"袋码")。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| æ‰«ç ç±»åž‹ | ç­‰å¾…扫码区下拉 | ä¸‰é€‰ä¸€ï¼šè¢‹ç /托盘码/批次码 |
| ç”Ÿäº§äº§çº¿ | æ‰«ç å¡é…ç½®åŒº + çŠ¶æ€å¡ | ä¸‹æ‹‰é€‰æ‹©ï¼Œå¯æ¸…空 |
| WIP æš‚存库区/库位 | æ‰«ç å¡é…ç½®åŒº + çŠ¶æ€å¡ | ä¸‹æ‹‰é€‰æ‹©ï¼Œåº“位随库区联动禁用/启用 |
| æœ€è¿‘入库追溯码 | ç»Ÿè®¡å¡ | å±•示最新一条扫码内容 |
| è¿½æº¯ç /类型/产线 | æœ€è¿‘扫码事件表 | è¡¨æ ¼æ–°å¢ž"类型"列,原"设备/产线"列改为"产线" |
| æœ€è¿‘扫码类型 | çŠ¶æ€å¡ | å±•示最新事件扫码类型 |
## æ³¨æ„äº‹é¡¹
1. åŽç«¯éœ€**执行 SQL è„šæœ¬** `sql/mysql/mes_pro_scan_event_scan_type.sql`(`mes_pro_scan_event` å¢žåŠ  `scan_type` åˆ—,`mes_pro_pallet` å¢žåŠ  `inbound_event_id`/`inbound_time` åˆ—),再**重新编译并重启**。
2. é‡å¯å‰æ—§æŽ¥å£ä¼šå¿½ç•¥ `scanType`(按袋码处理),托盘/批次码扫码会返回"袋码不存在";前端类型下拉照常可选,功能降级为仅袋码。
3. äº§çº¿/库区/库位下拉若接口 404(后端未重启),下拉为空,需先保障后端可用。
4. æ‰˜ç›˜ã€æ‰¹æ¬¡æ•´æ‰˜/整批入库会同步把其下未入库袋码标记为已入库,避免后续单袋扫码重复入库。
docs/seed_flow_trace_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
# ç§å­æµå‘信息(销售出库出入库流水) - H5 æº¯æºè”调说明
## èƒŒæ™¯
H5 æ‰«ç æº¯æºé¡µï¼ˆ`mes.trace-base-url` æŒ‡å‘çš„ `trace.html`)新增「种子流向信息」区块:
按**批次全流水**展示该批次的入库与出库记录(数据源为销售出库 `/sales-outbound/product-sales` æ¨¡å—对应的库存事务)。
同时修复既有「销售去向」数据链路:种子销售出库按袋码/托盘扫码拣货时,批次号只写入出库**明细表**
(`mes_wm_product_sales_detail.batch_code`),原查询只匹配出库**行表**批次号,导致溯源永远查不到销售出库信息。
## æ¶‰åŠæŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /public/mes/trace/query?code={追溯码} | H5 æº¯æºæŸ¥è¯¢ï¼ˆå…ç™»å½•),响应新增 `seedFlows` åŒºå— |
**响应新增字段 `seedFlows`(批次出入库流水,按事务时间升序):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| type | Integer | äº‹åŠ¡æ–¹å‘ï¼š1-入库 2-出库 |
| bizType | Integer | ä¸šåŠ¡ç±»åž‹ï¼ˆ118-生产入库、119-销售出库、124-产品产出、120-销售退货入库等,见 MesBizTypeConstants) |
| bizCode | String | æ¥æºä¸šåŠ¡å•å·ï¼ˆå¦‚é”€å”®å‡ºåº“å•å· SL_xxx) |
| quantity | BigDecimal | å˜åŠ¨æ•°é‡ï¼ˆå‡ºåº“ä¸ºè´Ÿæ•°ï¼Œå±•ç¤ºæ—¶å–ç»å¯¹å€¼ï¼‰ |
| transactionTime | LocalDateTime | äº‹åŠ¡å‘ç”Ÿæ—¶é—´ |
| warehouseName | String | ä»“库名称 |
| locationName | String | åº“区名称 |
| areaName | String | åº“位名称 |
| clientName | String | å®¢æˆ·åç§°ï¼ˆä»…销售出库流水带出,来自出库单快照) |
**`salesTargets` å­—段变化(同接口,向后兼容):**
- æŸ¥è¯¢å£å¾„扩展:批次号除匹配出库行外,同时按出库明细聚合匹配(排除已取消单据)。
- æ–°å¢žå¡«å…… `clientName`(客户名称快照)、`clientCode` ä¸å˜ï¼›æ­¤å‰æ’为 null。
## H5 é¡µé¢å±•示规则
| åŒºå— | å±•示条件 | åˆ— |
|------|----------|----|
| ç§å­æµå‘信息 | `seedFlows` éžç©ºï¼ˆä¼˜å…ˆï¼‰ | æ–¹å‘(入库/出库徽标)、业务、来源单号、客户、数量、时间、仓库/库区 |
| ç§å­æµå‘信息(回退) | `seedFlows` ä¸ºç©ºä½† `salesTargets` éžç©º | é”€å”®å‡ºåº“单、客户、数量、日期 |
## éƒ¨ç½²æ³¨æ„äº‹é¡¹
1. `trace.html` ä¸º `yudao-server` é™æ€èµ„源(`src/main/resources/static/trace.html`),需**重新打包发布后端**;
   è‹¥ç”Ÿäº§ nginx çš„ `/prod/trace.html`(如 `http://47.97.181.254:9008/prod/trace.html`)是独立拷贝部署的,需同步替换该文件。
2. é”€å”®å‡ºåº“流水的客户名称取 `mes_wm_product_sales.client_name` å¿«ç…§å­—段,要求环境库包含该列;
   ç»æŽ’查 `mom-thzy` åº“已有,**`mom-xgdl` åº“缺失 `client_name` / `outbound_time` / `outbound_user_id` åˆ—**(落后于代码),使用 local çŽ¯å¢ƒæ—¶éœ€å…ˆè¡¥åˆ—ã€‚
3. åŽ†å²å‡ºåº“å•ä¸ä¼šè¡¥ç”Ÿæˆæµæ°´ï¼Œä»…æ–°å‡ºåº“å¯çœ‹åˆ°å®Œæ•´å…¥+出记录;无流水时回退展示销售出库单。
## éªŒè¯æ•°æ®ï¼ˆmom-thzy)
批次 `PC20260819twoITEM_001`:入库 100(产品产出,虚拟线边仓库)→ å‡ºåº“ 1(销售出库 SL_20260901002,成品合格库,南通客户)。
docs/seed_qr_label_gb_compliance_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,159 @@
# ç§å­äºŒç»´ç æ ‡ç­¾å›½æ ‡åˆè§„ - å‰ç«¯è”调方案
## èƒŒæ™¯
依据《农作物种子标签二维码编码规则》与《农作物种子标签通则》(GB 20464-2024),
每个二维码对应唯一的最小销售单元种子且不得重复使用,二维码必须包含四项信息并按顺序排列:
1. å“ç§åç§°
2. ç”Ÿäº§ç»è¥è€…名称(进口种子为进口商名称)
3. å•元识别代码(不超过 20 ä¸ªå­—符,数字或字母数字组合)
4. è¿½æº¯ç½‘址
本次改造将「生产经营者名称」与「追溯网址」收敛为后端统一数据源,并让已作废的追溯码对外失效。
## æ¶‰åŠé¡µé¢
- H5 æ‰«ç æº¯æºé¡µ `trace.html`(已完成改造,无需前端另行处理)
- ç®¡ç†åŽå° - èµ‹ç ç®¡ç†é¡µï¼ˆèœå• `mes/pro/bagCode/index`,前端工程中**当前缺失**,需新建)
- ç®¡ç†åŽå° - æ‰˜ç›˜ç ç®¡ç†é¡µ / æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†é¡µï¼ˆäºŒç»´ç é¢„览与导出,如有)
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž | é‰´æƒ |
|------|------|------|------|
| GET | `/public/mes/trace/config` | **新增**:获得溯源页配置(生产经营者名称、追溯网址) | å…ç™»å½• |
| GET | `/public/mes/trace/query` | è¿½æº¯æŸ¥è¯¢ï¼Œ**响应新增 3 ä¸ªå­—段** | å…ç™»å½• |
| GET | `/admin-api/mes/pro/bag-code/export-qr` | å¯¼å‡ºäºŒç»´ç æ ‡ç­¾ ZIP,**新增校验与拒绝逻辑** | `mes:pro-bag-code:export` |
| GET | `/admin-api/mes/pro/bag-code/export-excel` | å¯¼å‡ºè¢‹ç  Excel | `mes:pro-bag-code:export` |
### 1. æ–°å¢žï¼šèŽ·å¾—æº¯æºé¡µé…ç½®
`GET /public/mes/trace/config`
**请求参数:** æ— 
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "operatorName": "新疆塔里木河种业股份有限公司",
    "traceUrl": "http://1.95.78.124:9008/prod/trace.html",
    "imported": false
  },
  "msg": ""
}
```
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| operatorName | String | ç”Ÿäº§ç»è¥è€…名称;`imported=true` æ—¶ä¸ºè¿›å£å•†åç§° |
| traceUrl | String | è¿½æº¯ç½‘址,与二维码内容前缀完全一致(不含 `code` å‚数) |
| imported | Boolean | æ˜¯å¦è¿›å£ç§å­åœºæ™¯ï¼›ä¸º true æ—¶é¡µé¢å­—段名应显示「进口商名称」 |
### 2. è¿½æº¯æŸ¥è¯¢å“åº”新增字段
`GET /public/mes/trace/query?code=xxx`
**响应字段新增:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| codeStatus | String | `VALID`-有效 / `CANCELED`-已作废 |
| operatorName | String | ç”Ÿäº§ç»è¥è€…名称(与标签印刷内容同源) |
| traceUrl | String | è¿½æº¯ç½‘址(与标签印刷内容同源) |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "sourceType": "BAG",
    "requestCode": "0286267849370923",
    "codeStatus": "VALID",
    "operatorName": "新疆塔里木河种业股份有限公司",
    "traceUrl": "http://1.95.78.124:9008/prod/trace.html",
    "batchBound": false,
    "itemCode": "xxx",
    "itemName": "塔河4号",
    "itemUnitMeasureName": "48千克"
  }
}
```
## å­—段展示规则
### H5 æº¯æºé¡µï¼ˆè¿½æº¯æŸ¥è¯¢åŒºå—)
四要素必须按国标顺序展示,其余信息排列其后:
| é¡ºåº | å­—段 | å–值来源 | è¯´æ˜Ž |
|------|------|----------|------|
| 1 | å“ç§åç§° | `itemName` + `itemSpecification` | å›½æ ‡è¦ç´ â‘  |
| 2 | ç”Ÿäº§ç»è¥è€… / è¿›å£å•†åç§° | `operatorName`,退回 `/config` çš„ `operatorName` | å›½æ ‡è¦ç´ â‘¡ï¼›`imported=true` æ—¶æ ‡ç­¾æ”¹ä¸ºã€Œè¿›å£å•†åç§°ã€ |
| 3 | å•元识别代码 | `requestCode` åŽ» URL åŽçš„码值 | å›½æ ‡è¦ç´ â‘¢ã€‚**仅袋码(`sourceType=BAG`)使用此名称**;托盘码/批次码展示为「托盘码(非最小销售单元)」「批次码(非最小销售单元)」,避免误导 |
| 4 | è¿½æº¯ç½‘址 | `traceUrl`,退回 `/config` çš„ `traceUrl` | å›½æ ‡è¦ç´ â‘£ï¼Œå¯ç‚¹å‡»è·³è½¬ |
| 5+ | è®¡é‡å•位、追溯类型、批次号、生产日期、产线 | å„自字段 | è¾…助信息 |
### H5 æº¯æºé¡µï¼ˆç”Ÿäº§æ˜Žç»†æŠ˜å åŒºï¼‰
国标要求追溯网址应可查询「种子加工批次」及「物流销售信息」,因此以下区块**对外展示**(默认折叠不展开):
| åŒºå— | æ•°æ®æ¥æº | è¯´æ˜Ž |
|------|----------|------|
| ç”Ÿäº§æ‰¹æ¬¡ä¿¡æ¯ | `batchCode`、`batchStatusName`、`produceDate`、`lineName`、`planQuantity` ç­‰ | å¯¹åº”国标「种子加工批次」;`batchBound=false`(未录入批次)时**不展示该区块**,避免暗示无加工批次 |
| ç§å­æµå‘信息 | `seedFlows`(销售出库 bizType=119)或 `salesTargets` | å¯¹åº”国标「物流销售信息」,含来源单号、客户、数量、仓库/库区 |
| åŠ å·¥æµè½¬ | `processTraces` | åŠ å·¥èŠ‚ç‚¹æµè½¬è®°å½• |
以下区块**仅内部可见,不对外展示**(如需一并展示,将 `detailSections(d, false)` æ”¹ä¸º `detailSections(d, true)`):
| åŒºå— | ä¸å±•示原因 |
|------|-----------|
| ç”Ÿäº§äººå‘˜ | æ¶‰åŠä¸ªäººä¿¡æ¯ |
| è´¨æ£€ä¿¡æ¯ã€æ‰˜ç›˜ä¿¡æ¯ã€è¢‹ç åˆ—表、采购来源 | å†…部明细,非消费者关切 |
### ç®¡ç†åŽå° - èµ‹ç ç®¡ç†é¡µ
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å“ç§åç§° | åˆ—表、详情 | è¢‹ç ç»‘定的品种 |
| å•元识别代码 | åˆ—表、详情 | å³è¢‹ç  `code` |
| çŠ¶æ€ | åˆ—表 | å·²ç”Ÿæˆ / å·²å¯¼å‡ºå°åˆ· / å·²ä½œåºŸ |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| å¯¼å‡ºäºŒç»´ç æ ‡ç­¾ | å››è¦ç´ ä»»ä¸€ç¼ºå¤±åˆ™**拒绝导出并报错**,不再静默跳过空字段 |
| å››è¦ç´ ç¼ºå¤±æç¤º | ç¼ºå°‘品种名称 â†’ 1040750021;缺少生产经营者 â†’ 1040750022;缺少单元识别代码 â†’ 1040750023;未配置追溯网址 â†’ 1040750020 |
| å•元识别代码格式 | å¿…须为 1~20 ä½æ•°å­—或字母数字组合,含连字符等符号时报 1040750024 |
| æ‰«ç å‘½ä¸­å·²ä½œåºŸè¢‹ç  | è¿”回 `codeStatus=CANCELED`,页面显示「该追溯码已作废,非本公司有效产品,谨防假冒」,且不再展示溯源详情 |
| å·²ä½œåºŸç è¢«æ‰«æ | **仍记录扫码事件**(作废码被扫是重要防伪信号),不计为查询失败 |
| æœªé…ç½®è¿½æº¯ç½‘址 | éž local çŽ¯å¢ƒå¯åŠ¨æ—¶ç›´æŽ¥å¤±è´¥ï¼›local çŽ¯å¢ƒå¯é€šè¿‡ `mes.allow-plain-qr-code=true` å…¼å®¹åŽ†å²ç  |
| å¯¼å‡ºäºŒç»´ç æ ‡ç­¾ | **已作废的袋码不再导出**(按批次/生成组导出时均排除)。作废码标签若印出并贴到产品上,消费者扫码会被提示「该追溯码已作废,谨防假冒」,导致真品被误判为假货 |
## é…ç½®é¡¹
后端配置项(`application-*.yaml` çš„ `mes` å‰ç¼€ï¼‰ï¼š
| é…ç½®é¡¹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|
| `mes.trace-base-url` | æ˜¯ | è¿½æº¯ç½‘址,必须为公众可访问地址。非 local çŽ¯å¢ƒè‹¥ä¸ºå†…ç½‘ IP / localhost / å ä½ç¬¦å°†**拒绝启动** |
| `mes.operator-name` | æ˜¯ | ç”Ÿäº§ç»è¥è€…名称。未配置则无法导出标签(启动时告警) |
| `mes.importer-name` | å¦ | è¿›å£å•†åç§°ï¼Œè¿›å£ç§å­åœºæ™¯å¡«å†™ |
| `mes.imported` | å¦ | æ˜¯å¦è¿›å£ç§å­åœºæ™¯ï¼Œé»˜è®¤ false |
| `mes.allow-plain-qr-code` | å¦ | æ˜¯å¦å…è®¸äºŒç»´ç ç¼–码纯码值,仅供本地兼容历史码,默认 false |
## æ³¨æ„äº‹é¡¹
- **前端禁止再硬编码生产经营者名称与追溯网址**,必须通过 `/public/mes/trace/config` èŽ·å–ï¼Œå¦åˆ™ä¼šä¸ŽäºŒç»´ç å†…å®¹ä¸ä¸€è‡´ã€‚
- è¿½æº¯ç½‘址在二维码内容与页面展示之间只有 `mes.trace-base-url` ä¸€ä¸ªçœŸå€¼æºï¼Œå‰ç«¯å±•示时不要自行拼接 `/prod` ç­‰è·¯å¾„。
- å·²ä½œåºŸç çš„判定覆盖三种入口:袋码(状态 30)、托盘码(状态 20)、批次码(状态 40),三者的作废枚举值不同,前端不要共用常量判断。
- **存量标签需重新导出**:改造前导出的二维码内容已固化,其中追溯网址可能为内网地址且标签缺少四要素,需重新导出印刷。
- **袋序号改为原子分配**:原先由「查库内最大值再 +1」得到,并发生成会产生重复袋序号(袋序号会印在标签上);现已改为 Redis åŽŸå­è‡ªå¢žåˆ†é…ï¼Œå¹¶ç”¨åº“å†…æœ€å¤§å€¼æ ¡æ­£åŸºå‡†ã€‚åºå·**只保证不重复、不回退,不保证连续**(并发下不同请求拿到不同区段,属正常)。
- ç®¡ç†åŽå°èµ‹ç ç®¡ç†é¡µé¢ï¼ˆ`mes/pro/bagCode/index`)在前端工程中缺失,业务人员目前无法自助查看/导出二维码,建议补充。
docs/seed_quality_bag_granularity.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
# ç§å­è´¨æ£€æŒ‰è¢‹ç ç²’度流转库存 - å‰ç«¯è”调方案
## èƒŒæ™¯
种子质检原逻辑:质检合格后**整批**将仓库批次库存从暂存库(10)转为合格库(20),一袋合格即整批放行,颗粒度过粗。
本次改造为**按袋码粒度**:质检只针对被扫的那一袋,合格仅将该袋对应库存从暂存库转入合格库,同批次其他袋保持暂存库待检;同时给每袋打上质检标记(`qc_status`),销售出库扫袋码/托盘时校验已质检合格才允许出库。
## æ¶‰åŠé¡µé¢
- è´¨é‡ç®¡ç†-种子质检(提交质检表单)
- é”€å”®å‡ºåº“-扫码拣货(扫袋码/托盘)
- åº“存台账(`mes_wm_material_stock` åˆ†é¡µï¼šåŒæ‰¹æ¬¡å¯èƒ½åŒæ—¶å‡ºçŽ°æš‚å­˜åº“ä¸Žåˆæ ¼åº“ä¸¤æ¡è®°å½•ï¼ŒçŠ¶æ€åˆ—å·²æŒ‰å­—å…¸å±•ç¤ºæ–‡å­—ï¼‰
## API
本次**无新增/修改接口**,均为既有接口的校验语义与数据流转变化。
### 1. æäº¤ç§å­è´¨æ£€
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/mes/qc/seed-quality/submit` | æäº¤ç§å­è´¨æ£€ï¼ˆæŒ‰è¢‹ç ï¼‰ |
**请求参数(不变):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | æ˜¯ | è¢‹ç ï¼ˆæ‰«ç å†…容),**必须为有效袋码** |
| batchCode | String | å¦ | ç”Ÿäº§æ‰¹æ¬¡å·ï¼ˆå…¼å®¹é¢„留) |
| indicators | Array | æ˜¯ | å››é¡¹æŒ‡æ ‡ï¼ˆpurity/cleanliness/germination_rate/moisture) |
| reason | String | å¦ | å¤‡æ³¨ |
**响应(不变):** `{ "code": 0, "data": è´¨æ£€è®°å½•ID }`
### 2. æ‰«ç æ‹£è´§
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/mes/wm/product-sales/scan` | æ‰«ç æ‹£è´§ï¼ˆè¢‹ç /托盘) |
**请求参数(不变):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| salesId | Long | æ˜¯ | é”€å”®å‡ºåº“单ID |
| scanContent | String | æ˜¯ | è¢‹ç æˆ–托盘码 |
| quantity | BigDecimal | å¦ | å‡ºåº“数量(缺省用行数量) |
**响应(不变):** æ²¿ç”¨ `MesWmProductSalesScanRespVO`,`availableQuantity`/`outboundQuantity` å‡åŸºäºŽ**合格库**库存计算。
## ä¸šåŠ¡è§„åˆ™å˜åŒ–
| åœºæ™¯ | åŽŸè¡Œä¸º | æ–°è¡Œä¸º |
|------|--------|--------|
| ç§å­è´¨æ£€æäº¤éžè¢‹ç  | å…¼å®¹æ‰¹æ¬¡ç æäº¤ | æŠ¥é”™ã€Œæœªè§£æžåˆ°æœ‰æ•ˆè¢‹ç ï¼Œç§å­è´¨æ£€å¿…须以袋码提交」 |
| è´¨æ£€åˆæ ¼ | æ•´æ‰¹æš‚存库→合格库 | ä»…该袋:暂存库 -1、合格库 +1(同批次其他袋仍为暂存库);袋码 qc_status=20 |
| è´¨æ£€ä¸åˆæ ¼ | æ— åº“存变化 | è¯¥è¢‹ä»Žæš‚存库存清除(报废);袋码 qc_status=30 |
| é”€å”®å‡ºåº“扫袋码 | ä¸æ ¡éªŒè´¨æ£€ | qc_status å¿…须为 20,否则报「该袋码未质检合格,禁止出库」 |
| é”€å”®å‡ºåº“扫托盘 | ä¸æ ¡éªŒè´¨æ£€ | æ‰˜ç›˜ä¸‹æ‰€æœ‰è¢‹ç  qc_status å¿…须为 20,否则报「托盘下存在未质检袋码,禁止整托出库」 |
| é”€å”®å‡ºåº“选库存 | å¯èƒ½é€‰ä¸­æš‚存库记录 | ä»…从合格库(20)记录中选数量足够的 |
## åº“存语义说明
同一次(入库仍按批次聚合)质检合格后,同批次 `mes_wm_material_stock` ä¼šåŒæ—¶å­˜åœ¨ä¸¤æ¡è®°å½•:
| è®°å½• | stock_state | å«ä¹‰ | æ˜¯å¦å¯å‡ºåº“ |
|------|-------------|------|-----------|
| æš‚存库 | 10 | è¯¥æ‰¹æ¬¡æœªè´¨æ£€çš„剩余袋数 | å¦ |
| åˆæ ¼åº“ | 20 | å·²è´¨æ£€åˆæ ¼ã€å¯é”€å”®çš„袋数 | æ˜¯ |
示例:批次入库 3 è¢‹ï¼ˆquantity=3、暂存库)。质检合格 1 è¢‹åŽ â†’ æš‚存库 quantity=2 + åˆæ ¼åº“ quantity=1。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| è´¨æ£€çŠ¶æ€ï¼ˆqc_status) | ç§å­è´¨æ£€æäº¤ç»“果反馈 | å‰ç«¯æ— éœ€å±•示,后端在第二次提交同一袋码时报「该袋码已质检,禁止重复提交」 |
| åº“存状态 | åº“存台账列表 | åŒä¸€ç‰©æ–™/批次可能出现两条不同状态记录,均为正常现象 |
| å¯å‡ºåº“数量 | æ‰«ç æ‹£è´§ç»“æžœ | ä»¥ `availableQuantity` ä¸ºå‡†ï¼Œåªåæ˜ åˆæ ¼åº“库存 |
## æ³¨æ„äº‹é¡¹
1. **种子质检提交必须传袋码**:`code` å­—段现在必须是系统中存在的有效袋码;仅传批次号不再被接受。
2. **质检成功后库存总数不变**:暂存库 -1 ä¸Žåˆæ ¼åº“ +1 åŒæ­¥å‘生,物料在库总数守恒,只是从"待检"变为"可售"。
3. **同批次双状态并存**:暂存库与合格库记录同时存在是预期行为,前端在库存台账、出库选库存时应能接受同一物料/批次出现多行。
4. **出库数量提示**:若某批次仅部分袋质检合格,扫码拣货可出库数量只包含合格袋,前端提示应以接口返回的 `availableQuantity` ä¸ºå‡†ã€‚
5. **不合格袋报废**:质检不合格的袋会从暂存库存中扣除,且该袋码 qc_status=30,禁止出库。
docs/sql/pro_batch_processing_team.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
-- æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†ï¼šæ–°å¢žã€ŒåŠ å·¥ç­ç»„ã€å­—æ®µ
-- æ‰§è¡Œåº“:mom-thzy(local)
ALTER TABLE mes_pro_batch ADD COLUMN processing_team VARCHAR(64) NULL COMMENT '加工班组(手工填写)' AFTER line_name;
docs/sql/trace_platform_01_schema.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
-- æ‰¹æ¬¡è¿½æº¯æ•°æ®æ¨¡åž‹è¡¥å¼º
-- è¯´æ˜Žï¼šå­—段先允许为空,历史数据回填完成后再评估是否收紧约束。
-- æ³¨æ„ï¼šé¡¹ç›®ä¸ä½¿ç”¨ tenant_id;执行前请确认字段和索引不存在。
ALTER TABLE mes_wm_batch
    ADD COLUMN pro_batch_id BIGINT NULL COMMENT '对应生产批次ID' AFTER id;
ALTER TABLE mes_wm_batch
    ADD INDEX idx_mes_wm_batch_pro_batch_id (pro_batch_id);
ALTER TABLE mes_wm_sn
    ADD COLUMN pro_batch_id BIGINT NULL COMMENT '生产批次ID' AFTER id;
ALTER TABLE mes_wm_sn
    ADD COLUMN bag_code_id BIGINT NULL COMMENT '袋码ID' AFTER pro_batch_id;
ALTER TABLE mes_wm_sn
    ADD COLUMN pallet_id BIGINT NULL COMMENT '托盘ID' AFTER bag_code_id;
ALTER TABLE mes_wm_sn
    ADD COLUMN sales_id BIGINT NULL COMMENT '销售出库单ID' AFTER pallet_id;
ALTER TABLE mes_wm_sn
    ADD INDEX idx_mes_wm_sn_pro_batch_id (pro_batch_id);
ALTER TABLE mes_wm_sn
    ADD INDEX idx_mes_wm_sn_bag_code_id (bag_code_id);
ALTER TABLE mes_qc_seed_quality
    ADD COLUMN batch_id BIGINT NULL COMMENT '生产批次ID' AFTER item_id;
ALTER TABLE mes_qc_seed_quality
    ADD INDEX idx_mes_qc_seed_quality_batch_id (batch_id);
CREATE TABLE mes_trace_event (
    id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
    event_no VARCHAR(64) NOT NULL COMMENT '事件编号',
    event_type VARCHAR(32) NOT NULL COMMENT '事件类型',
    event_category VARCHAR(32) NULL COMMENT '事件分类',
    event_time DATETIME NOT NULL COMMENT '事件时间',
    pro_batch_id BIGINT NULL COMMENT '生产批次ID',
    wm_batch_id BIGINT NULL COMMENT '仓储批次ID',
    bag_code_id BIGINT NULL COMMENT '袋码ID',
    pallet_id BIGINT NULL COMMENT '托盘ID',
    sn_id BIGINT NULL COMMENT 'SN ID',
    item_id BIGINT NULL COMMENT '物料ID',
    biz_type VARCHAR(32) NULL COMMENT '业务类型',
    biz_id BIGINT NULL COMMENT '业务ID',
    biz_code VARCHAR(100) NULL COMMENT '业务编码',
    biz_line_id BIGINT NULL COMMENT '业务明细ID',
    from_node_type VARCHAR(32) NULL COMMENT '来源节点类型',
    from_node_id BIGINT NULL COMMENT '来源节点ID',
    from_node_code VARCHAR(100) NULL COMMENT '来源节点编码',
    to_node_type VARCHAR(32) NULL COMMENT '目标节点类型',
    to_node_id BIGINT NULL COMMENT '目标节点ID',
    to_node_code VARCHAR(100) NULL COMMENT '目标节点编码',
    quantity DECIMAL(18, 6) NULL COMMENT '数量',
    warehouse_id BIGINT NULL COMMENT '仓库ID',
    location_id BIGINT NULL COMMENT '库区ID',
    area_id BIGINT NULL COMMENT '库位ID',
    line_id BIGINT NULL COMMENT '产线ID',
    work_order_id BIGINT NULL COMMENT '工单ID',
    process_id BIGINT NULL COMMENT '工序ID',
    sales_id BIGINT NULL COMMENT '销售出库单ID',
    quality_id BIGINT NULL COMMENT '质检记录ID',
    operator_id BIGINT NULL COMMENT '操作人ID',
    payload_json JSON NULL COMMENT '事件扩展数据',
    content_hash VARCHAR(64) NULL COMMENT '事件内容SHA-256摘要',
    prev_hash VARCHAR(64) NULL COMMENT '前一事件摘要',
    creator VARCHAR(64) NULL DEFAULT '' COMMENT '创建者',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updater VARCHAR(64) NULL DEFAULT '' COMMENT '更新者',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    deleted BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (id),
    UNIQUE KEY uk_mes_trace_event_no (event_no),
    KEY idx_mes_trace_event_batch_time (pro_batch_id, event_time),
    KEY idx_mes_trace_event_biz (biz_type, biz_id, biz_line_id),
    KEY idx_mes_trace_event_nodes (from_node_id, to_node_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES统一追溯事件';
-- åŽ†å²æ•°æ®å›žå¡«ç¤ºä¾‹ï¼Œç¡®è®¤æ•°æ®è´¨é‡åŽæ‰§è¡Œï¼š
-- UPDATE mes_wm_batch w JOIN mes_pro_batch p ON p.code = w.code AND p.deleted = b'0'
-- SET w.pro_batch_id = p.id WHERE w.pro_batch_id IS NULL AND w.deleted = b'0';
-- UPDATE mes_qc_seed_quality q JOIN mes_pro_batch p ON p.code = q.batch_code AND p.deleted = b'0'
-- SET q.batch_id = p.id WHERE q.batch_id IS NULL AND q.deleted = b'0';
docs/wm_transaction_query.md
@@ -9,6 +9,7 @@
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/wm/transaction/page | åˆ†é¡µæŸ¥è¯¢åº“存事务流水 |
| GET | /mes/pro/batch/get-by-code | ç‚¹å‡»æ‰¹æ¬¡å·æŸ¥è¯¢ç”Ÿäº§æ‰¹æ¬¡ã€æ‰˜ç›˜åŠè¢‹ç ä¿¡æ¯ |
### è¯·æ±‚参数
@@ -23,10 +24,37 @@
| bizType | Integer | å¦ | ä¸šåŠ¡ç±»åž‹ |
| bizCode | String | å¦ | ä¸šåŠ¡å•å·ï¼Œæ¨¡ç³ŠåŒ¹é… |
| materialStockId | Long | å¦ | åº“存记录ID |
| bagCode | String | å¦ | è¢‹ç ï¼Œæ¨¡ç³ŠåŒ¹é…ï¼ˆè´¨æ£€ã€é”€å”®å‡ºåº“等袋码级流水) |
| startTime | String | å¦ | å¼€å§‹æ—¶é—´ï¼ˆæ ¼å¼ï¼šyyyy-MM-dd HH:mm:ss) |
| endTime | String | å¦ | ç»“束时间(格式:yyyy-MM-dd HH:mm:ss) |
| pageNo | Integer | æ˜¯ | é¡µç ï¼Œé»˜è®¤1 |
| pageSize | Integer | æ˜¯ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
### æ‰¹æ¬¡è¿½æº¯æŸ¥è¯¢
点击流水列表中的批次号时,使用当前行的 `batchCode` è°ƒç”¨ä»¥ä¸‹æŽ¥å£ï¼š
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | æ˜¯ | æ‰¹æ¬¡å·æˆ–批次二维码内容 |
请求示例:`GET /mes/pro/batch/get-by-code?code={batchCode}`
接口返回的重点字段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ç”Ÿäº§æ‰¹æ¬¡ID |
| code | String | æ­£å¼æ‰¹æ¬¡å· |
| tempCode | String | ä¸´æ—¶æ‰¹æ¬¡ç  |
| itemCode | String | ç‰©æ–™ç¼–码 |
| itemName | String | ç‰©æ–™åç§° |
| workers | Array | æ‰¹æ¬¡äººå‘˜é…ç½® |
| pallets | Array | æ‰¹æ¬¡ä¸‹çš„æ‰˜ç›˜åˆ—表 |
| bagTotal | Integer | è¢‹ç æ€»æ•° |
| bags | Array | æ‰¹æ¬¡ä¸‹çš„袋码列表 |
该接口同时支持正式批次号和临时批次码。若批次号为空,不显示点击入口;若查询不到生产批次,按接口返回的业务错误提示用户。
### å“åº”字段
@@ -44,6 +72,8 @@
| quantity | BigDecimal | å˜åŠ¨æ•°é‡ï¼ˆæ­£æ•°=入库,负数=出库) |
| batchId | Long | æ‰¹æ¬¡ID |
| batchCode | String | æ‰¹æ¬¡å· |
| bagCodeId | Long | è¢‹ç ID(袋码级流水非空) |
| bagCode | String | è¢‹ç ï¼ˆè¢‹ç çº§æµæ°´éžç©ºï¼Œå¦‚质检、销售出库、移库) |
| warehouseId | Long | ä»“库ID |
| warehouseName | String | ä»“库名称 |
| locationId | Long | åº“区ID |
@@ -111,6 +141,24 @@
| 121 | å¤–协发料 | å¤–协发料 |
| 122 | å¤–协入库 | å¤–协入库 |
## è°ƒç”¨æ—¶åº
1. è¿›å…¥åº“存事务流水页面时,调用 `/mes/wm/transaction/page` èŽ·å–æµæ°´åˆ—è¡¨ã€‚
2. åˆ—表中展示 `batchCode`;批次号非空时允许点击。
3. ç‚¹å‡»æ‰¹æ¬¡å·åŽï¼Œå°†å½“前行的 `batchCode` ä½œä¸º `code` è°ƒç”¨ `/mes/pro/batch/get-by-code`。
4. æ ¹æ®è¿”回结果展示生产批次信息、托盘列表和袋码列表;`bagTotal` ç”¨äºŽæ˜¾ç¤ºè¢‹ç æ€»æ•°ï¼Œ`bags` ç”¨äºŽå±•示袋码明细。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| batchCode | åº“存流水列表 | æ‰¹æ¬¡å·éžç©ºæ—¶ä½œä¸ºå¯ç‚¹å‡»å…¥å£ |
| code | æ‰¹æ¬¡è¯¦æƒ… | æ­£å¼æ‰¹æ¬¡å· |
| tempCode | æ‰¹æ¬¡è¯¦æƒ… | ä¸´æ—¶æ‰¹æ¬¡ç ï¼Œæœ‰å€¼æ—¶å±•示 |
| bagTotal | æ‰¹æ¬¡è¯¦æƒ… | å±•示该批次袋码总数 |
| bags[].code | æ‰¹æ¬¡è¯¦æƒ… | å±•示袋码明细 |
| pallets | æ‰¹æ¬¡è¯¦æƒ… | å±•示该批次关联的托盘信息 |
## æœç´¢æ¡ä»¶
| æ¡ä»¶å­—段 | ç»„件类型 | è¯´æ˜Ž |
@@ -140,6 +188,7 @@
| åº“区 | locationName | 100 | - |
| åº“位 | areaName | 100 | - |
| äº‹åŠ¡æ—¶é—´ | transactionTime | 160 | æ ¼å¼ï¼šyyyy-MM-dd HH:mm:ss |
| è¢‹ç  | bagCode | 150 | è¢‹ç çº§æµæ°´çš„袋码;批次/袋码可继续追溯 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
@@ -150,6 +199,7 @@
| æ•°é‡æ˜¾ç¤º | quantity > 0 æ˜¾ç¤ºç»¿è‰²ï¼Œquantity < 0 æ˜¾ç¤ºçº¢è‰² |
| ç±»åž‹æ ‡ç­¾ | å…¥åº“显示绿色 success æ ‡ç­¾ï¼Œå‡ºåº“显示红色 danger æ ‡ç­¾ |
| ä¸šåŠ¡å•å·è·³è½¬ | ç‚¹å‡» bizCode å¯è·³è½¬åˆ°å¯¹åº”业务单据详情页 |
| è¢‹ç ç­›é€‰ | ä¼  bagCode å¯æŸ¥çœ‹æŒ‡å®šè¢‹ç çš„完整流转(质检暂存出库/合格入库/销售出库等) |
## æƒé™é…ç½®
img/0d9430aac760f2a00123f8f4f9b8eae0.jpg
img/8fbfd97ac926a2dfd7e3bc2577aee946.jpg
img/ChatGPT Image 2026Äê9ÔÂ30ÈÕ 18_10_01.png
img/tahe_seed_logo.png
sql/mysql/mes_pro_bag_code_qc_status.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
-- =========================================
-- è¢‹ç è´¨æ£€çŠ¶æ€ï¼šæ”¯æŒç§å­è´¨æ£€æŒ‰è¢‹ç ç²’åº¦æµè½¬åº“å­˜
-- qc_status:10 æœªè´¨æ£€ / 20 è´¨æ£€åˆæ ¼ / 30 è´¨æ£€ä¸åˆæ ¼
-- æ–°å¢žåˆ—用于记录每袋的质检结果,销售出库扫袋码时校验该袋已质检合格。
-- =========================================
ALTER TABLE `mes_pro_bag_code`
    ADD COLUMN `qc_status` TINYINT NOT NULL DEFAULT 10 COMMENT '质检状态(10未质检 20质检合格 30质检不合格)' AFTER `status`;
-- å›žå¡«å­˜é‡æ•°æ®ï¼šæŒ‰å·²æœ‰ç§å­è´¨æ£€è®°å½•同步袋码质检状态
UPDATE `mes_pro_bag_code` b
    JOIN `mes_qc_seed_quality` q ON b.code = q.code AND b.deleted = 0
SET b.qc_status = IF(q.result = 1, 20, 30), b.update_time = NOW();
sql/mysql/mes_pro_scan_event_scan_type.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
-- ç”Ÿäº§çº¿æ‰«ç å…¥åº“:支持袋码/托盘码/批次码三种扫码类型,并去掉设备编码必填
-- è¯´æ˜Žï¼š
-- 1. mes_pro_scan_event å¢žåŠ  scan_type åˆ—,标记扫码类型(BAG è¢‹ç  / PALLET æ‰˜ç›˜ç  / BATCH æ‰¹æ¬¡ç ï¼‰ï¼ŒåŽ†å²è¢‹ç æ•°æ®å›žå¡« BAG
-- 2. mes_pro_pallet å¢žåŠ å…¥åº“æ ‡è®°åˆ—ï¼ˆæ‰˜ç›˜åŽŸå…ˆæ— å…¥åº“æ ‡è®°å­—æ®µï¼Œæ•´æ‰˜å…¥åº“éœ€åŽ»é‡ï¼Œé˜²æ­¢åŒä¸€æ‰˜ç›˜é‡å¤æ‰«ç å…¥åº“ï¼‰
ALTER TABLE mes_pro_scan_event ADD COLUMN scan_type VARCHAR(16) NULL COMMENT '扫码类型(BAG袋码/PALLET托盘码/BATCH批次码)' AFTER bag_code;
-- è®¾å¤‡ç¼–码不再必填(前端去掉设备编码,历史列保持兼容)
ALTER TABLE mes_pro_scan_event MODIFY COLUMN device_code VARCHAR(64) NULL DEFAULT NULL COMMENT '设备编码(不再必填)';
ALTER TABLE mes_pro_pallet ADD COLUMN inbound_event_id BIGINT NULL COMMENT '最近入库扫码事件编号' AFTER bag_count;
ALTER TABLE mes_pro_pallet ADD COLUMN inbound_time DATETIME NULL COMMENT '最近入库时间' AFTER inbound_event_id;
UPDATE mes_pro_scan_event SET scan_type = 'BAG' WHERE scan_type IS NULL;
sql/mysql/mes_qc_seed_quality_record_menu.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
-- =============================================
-- ç§å­è´¨æ£€è®°å½•独立菜单
-- å°†è´¨æ£€è®°å½•从「种子质检」页(提交+记录合一)拆出,独立成菜单页面
-- è¯´æ˜Žï¼š
-- 1. ç§å­è´¨æ£€æäº¤é¡µï¼šseed-quality -> mes/qc/seedQuality/index(仅提交)
-- 2. è´¨æ£€è®°å½•页:seed-quality-record -> mes/qc/seedQuality/record(仅记录查询)
-- æŒ‚在「质量管理」(5500) ç›®å½•下;幂等,可重复执行;无租户字段。
-- =============================================
INSERT INTO system_menu
(id, name, parent_id, type, sort, status, path, icon, component, component_name, permission, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 1065224, '质检记录', 5500, 2, 95, 0, 'seed-quality-record', 'ep:list', 'mes/qc/seedQuality/record', 'MesQcSeedQualityRecord', '', b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE id = 1065224 AND deleted = b'0');
sql/mysql/mes_wm_material_stock_bag.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
CREATE TABLE mes_wm_material_stock_bag (
    id BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    material_stock_id BIGINT NOT NULL COMMENT '库存汇总记录ID',
    bag_code_id BIGINT NOT NULL COMMENT '生产袋码ID',
    bag_code VARCHAR(64) NOT NULL COMMENT '袋码',
    item_id BIGINT NOT NULL COMMENT '物料ID',
    batch_id BIGINT NULL COMMENT '仓库批次ID',
    batch_code VARCHAR(64) NULL COMMENT '批次号',
    vendor_id BIGINT NULL COMMENT '供应商ID',
    warehouse_id BIGINT NOT NULL COMMENT '仓库ID',
    location_id BIGINT NULL COMMENT '库区ID',
    area_id BIGINT NULL COMMENT '库位ID',
    stock_state TINYINT NOT NULL COMMENT '库存状态:10暂存,20合格,30已出库,40报废',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '明细状态:1在库,2已出库,3已报废',
    quantity DECIMAL(18, 6) NOT NULL DEFAULT 1 COMMENT '袋码库存数量',
    frozen BIT NOT NULL DEFAULT 0 COMMENT '是否冻结',
    last_transaction_id BIGINT NULL COMMENT '最近库存事务ID',
    inbound_time DATETIME NULL COMMENT '入库时间',
    outbound_time DATETIME NULL COMMENT '出库时间',
    creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
    PRIMARY KEY (id),
    UNIQUE KEY uk_bag_code (bag_code),
    KEY idx_material_stock (material_stock_id),
    KEY idx_batch_state (batch_id, stock_state, status),
    KEY idx_location_state (warehouse_id, location_id, area_id, stock_state, status),
    KEY idx_bag_code_id (bag_code_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES库存袋码明细';
sql/mysql/mes_wm_product_sales_add_columns.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
-- MES é”€å”®å‡ºåº“单表结构修改
-- æ ¹æ® MesWmProductSalesDO è¡¥å……缺失字段
-- è¯´æ˜Žï¼šä¿®å¤æŸ¥è¯¢æŠ¥é”™ Unknown column 'client_name',同时补充 outbound_time、outbound_user_id
-- 1. ç»é”€å•†åç§°å¿«ç…§
ALTER TABLE mes_wm_product_sales ADD COLUMN client_name VARCHAR(128) DEFAULT NULL COMMENT '经销商名称快照' AFTER client_id;
-- 2. å®žé™…出库时间
ALTER TABLE mes_wm_product_sales ADD COLUMN outbound_time DATETIME DEFAULT NULL COMMENT '实际出库时间';
-- 3. æ‰§è¡Œå‡ºåº“操作人ID
ALTER TABLE mes_wm_product_sales ADD COLUMN outbound_user_id BIGINT DEFAULT NULL COMMENT '执行出库操作人ID';
sql/mysql/mes_wm_qualified_warehouse.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
-- =====================================================================
-- æ–°å»ºåˆæ ¼ä»“库 + ä»“库类型 + è´¨æ£€å…¥åˆæ ¼åº“改造
--
-- 1) mes_wm_warehouse å¢žåŠ  type å­—段(10 æš‚存库 / 20 åˆæ ¼åº“)
-- 2) çŽ°æœ‰ä»“åº“åˆ†ç±»ï¼šè™šæ‹Ÿçº¿è¾¹ä»“åº“=10(暂存库),产品库/物料库=20(合格库)
-- 3) æ–°å»º"成品合格库"(type=20) + ä¸€ä¸ªåº“区 + ä¸€ä¸ªåº“位
-- 4) è™šæ‹Ÿä»“库中 stock_state=20 çš„合格库存迁移到新合格库
--
-- è¯´æ˜Žï¼šæœ¬è„šæœ¬å¹‚ç­‰ï¼Œå¯é‡å¤æ‰§è¡Œï¼›æ— ç§Ÿæˆ·å­—æ®µã€‚
-- =====================================================================
-- 1) å¢žåŠ ä»“åº“ç±»åž‹å­—æ®µï¼ˆMySQL ä¸æ”¯æŒ ALTER ADD COLUMN IF NOT EXISTS,先查存在性)
SET @type_exists = (
  SELECT COUNT(*)
  FROM information_schema.columns
  WHERE table_schema = DATABASE()
    AND table_name = 'mes_wm_warehouse'
    AND column_name = 'type'
);
SET @type_sql = IF(
  @type_exists = 0,
  'ALTER TABLE mes_wm_warehouse ADD COLUMN type tinyint NOT NULL DEFAULT 10 COMMENT ''仓库类型:10暂存库,20合格库'' AFTER frozen',
  'SELECT 1'
);
PREPARE type_stmt FROM @type_sql;
EXECUTE type_stmt;
DEALLOCATE PREPARE type_stmt;
-- 2) çŽ°æœ‰ä»“åº“åˆ†ç±»
UPDATE mes_wm_warehouse SET type = 10 WHERE code = 'WIP_VIRTUAL_WAREHOUSE' AND deleted = 0;
UPDATE mes_wm_warehouse SET type = 20 WHERE code IN ('1212', 'WHS000001') AND deleted = 0;
-- 3) æ–°å»º"成品合格库"(type=20)
SET @wh_exists = (
  SELECT COUNT(*) FROM mes_wm_warehouse WHERE code = 'FG_QUALIFIED_WH' AND deleted = 0
);
INSERT INTO mes_wm_warehouse (code, name, frozen, type, remark)
SELECT 'FG_QUALIFIED_WH', '成品合格库', 0, 20, '质检合格直接入库的目标仓库(type=20 åˆæ ¼åº“)'
WHERE @wh_exists = 0;
SET @wh_id = (SELECT id FROM mes_wm_warehouse WHERE code = 'FG_QUALIFIED_WH' AND deleted = 0 LIMIT 1);
-- æ–°å»º"成品合格库区"
SET @loc_exists = (
  SELECT COUNT(*) FROM mes_wm_warehouse_location WHERE code = 'FG_QUALIFIED_WH_LOC' AND deleted = 0
);
INSERT INTO mes_wm_warehouse_location (code, name, warehouse_id, frozen, remark)
SELECT 'FG_QUALIFIED_WH_LOC', '成品合格库区', @wh_id, 0, '质检合格入库默认库区'
WHERE @loc_exists = 0;
SET @loc_id = (SELECT id FROM mes_wm_warehouse_location WHERE code = 'FG_QUALIFIED_WH_LOC' AND deleted = 0 LIMIT 1);
-- æ–°å»º"成品合格库位"
SET @area_exists = (
  SELECT COUNT(*) FROM mes_wm_warehouse_area WHERE code = 'FG_QUALIFIED_WH_AREA' AND deleted = 0
);
INSERT INTO mes_wm_warehouse_area (code, name, location_id, frozen, remark)
SELECT 'FG_QUALIFIED_WH_AREA', '成品合格库位', @loc_id, 0, '质检合格入库默认库位'
WHERE @area_exists = 0;
SET @area_id = (SELECT id FROM mes_wm_warehouse_area WHERE code = 'FG_QUALIFIED_WH_AREA' AND deleted = 0 LIMIT 1);
-- 4) è¿ç§»è™šæ‹Ÿä»“库合格库存到新合格库
-- 4.1 è‹¥æ–°åˆæ ¼åº“已存在同 (item_id, batch_id, vendor_id) çš„ state=20 è®°å½•,先数量累加合并
UPDATE mes_wm_material_stock t
JOIN mes_wm_material_stock s
  ON s.warehouse_id = 3 AND s.stock_state = 20 AND s.deleted = 0
 AND t.warehouse_id = @wh_id AND t.stock_state = 20 AND t.deleted = 0
 AND t.item_id = s.item_id
 AND IFNULL(t.batch_id, 0) = IFNULL(s.batch_id, 0)
 AND IFNULL(t.vendor_id, 0) = IFNULL(s.vendor_id, 0)
SET t.quantity = t.quantity + s.quantity,
    t.reserved_quantity = t.reserved_quantity + s.reserved_quantity,
    t.frozen_quantity = t.frozen_quantity + s.frozen_quantity;
-- 4.2 åˆ é™¤å·²åˆå¹¶åˆ°æ–°åˆæ ¼åº“的源记录(虚拟仓库中)
DELETE s FROM mes_wm_material_stock s
JOIN mes_wm_material_stock t
  ON s.warehouse_id = 3 AND s.stock_state = 20 AND s.deleted = 0
 AND t.warehouse_id = @wh_id AND t.stock_state = 20 AND t.deleted = 0
 AND t.item_id = s.item_id
 AND IFNULL(t.batch_id, 0) = IFNULL(s.batch_id, 0)
 AND IFNULL(t.vendor_id, 0) = IFNULL(s.vendor_id, 0);
-- 4.3 å…¶ä½™åˆæ ¼åº“存直接改仓库/库区/库位
UPDATE mes_wm_material_stock
SET warehouse_id = @wh_id, location_id = @loc_id, area_id = @area_id
WHERE warehouse_id = 3 AND stock_state = 20 AND deleted = 0;
sql/mysql/mes_wm_transaction_bag_code.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
SET @exists_bag_code_id = (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_wm_transaction' AND column_name = 'bag_code_id');
SET @sql_bag_code_id = IF(@exists_bag_code_id = 0, 'ALTER TABLE mes_wm_transaction ADD COLUMN bag_code_id BIGINT NULL COMMENT ''生产袋码ID'' AFTER material_stock_id', 'SELECT 1');
PREPARE stmt FROM @sql_bag_code_id;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @exists_bag_code = (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mes_wm_transaction' AND column_name = 'bag_code');
SET @sql_bag_code = IF(@exists_bag_code = 0, 'ALTER TABLE mes_wm_transaction ADD COLUMN bag_code VARCHAR(64) NULL COMMENT ''袋码'' AFTER bag_code_id', 'SELECT 1');
PREPARE stmt FROM @sql_bag_code;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @exists_bag_code_index = (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'mes_wm_transaction' AND index_name = 'idx_bag_code');
SET @sql_bag_code_index = IF(@exists_bag_code_index = 0, 'ALTER TABLE mes_wm_transaction ADD KEY idx_bag_code (bag_code)', 'SELECT 1');
PREPARE stmt FROM @sql_bag_code_index;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
sql/mysql/sp_clean_business_data.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,435 @@
-- ============================================================
-- ä¸šåŠ¡æ•°æ®æ¸…ç†å­˜å‚¨è¿‡ç¨‹ï¼ˆç›®æ ‡åº“ï¼šmom-thzy)
--
-- ç”¨é€”:清空各业务模块的流水/事务数据,保留用户、角色、菜单、
--       å­—典、流程定义、基础配置与主数据档案。
--
-- æ³¨æ„ï¼š
-- 1. TRUNCATE ä¸º DDL éšå¼æäº¤ï¼Œæ— æ³•回滚,执行前请先备份。
-- 2. æ‰§è¡Œæ–¹å¼ï¼šmysql -uroot -p mom-thzy < sp_clean_business_data.sql
--    æˆ–在客户端中整体执行本脚本(含 DELIMITER)。
-- 3. æ‰§è¡Œæ¸…理:CALL sp_clean_business_data();
-- ============================================================
DROP PROCEDURE IF EXISTS `sp_clean_business_data`;
DELIMITER $$
CREATE DEFINER=`root`@`%` PROCEDURE `sp_clean_business_data`()
BEGIN
    SET FOREIGN_KEY_CHECKS = 0;
    -- ============================================================
    -- 1. ç³»ç»Ÿè¿è¡Œæ•°æ®ï¼ˆæ—¥å¿— / Token / éªŒè¯ç  / ç«™å†…ä¿¡ / ç¤¾äº¤ç»‘定)
    -- ============================================================
    TRUNCATE TABLE system_login_log;
    TRUNCATE TABLE system_operate_log;
    TRUNCATE TABLE system_notify_message;
    TRUNCATE TABLE system_oauth2_access_token;
    TRUNCATE TABLE system_oauth2_refresh_token;
    TRUNCATE TABLE system_oauth2_code;
    TRUNCATE TABLE system_oauth2_approve;
    TRUNCATE TABLE system_sms_code;
    TRUNCATE TABLE system_sms_log;
    TRUNCATE TABLE system_mail_log;
    TRUNCATE TABLE system_social_user;
    TRUNCATE TABLE system_social_user_bind;
    TRUNCATE TABLE infra_api_access_log;
    TRUNCATE TABLE infra_api_error_log;
    TRUNCATE TABLE infra_job_log;
    -- åˆ é™¤ä¸Žæœ¬é¡¹ç›®æ— å…³çš„æ¡†æž¶è‡ªå¸¦å®šæ—¶ä»»åŠ¡ï¼ˆpay/mall/iot/demo),
    -- ä¿ç•™æ—¥å¿—清理三件套:accessLogCleanJob / errorLogCleanJob / jobLogCleanJob
    DELETE FROM infra_job
    WHERE handler_name IN (
        'payNotifyJob', 'payOrderSyncJob', 'payOrderExpireJob',
        'payRefundSyncJob', 'payTransferSyncJob',
        'tradeOrderAutoCancelJob', 'tradeOrderAutoReceiveJob', 'tradeOrderAutoCommentJob',
        'brokerageRecordUnfreezeJob', 'combinationRecordExpireJob',
        'couponExpireJob', 'productStatisticsJob',
        'iotDeviceOfflineCheckJob', 'iotOtaUpgradeJob',
        'demoJob'
    );
    -- Quartz è°ƒåº¦è¿è¡Œæ—¶æ•°æ®ï¼ˆqrtz_locks ä¿ç•™é”è¡Œï¼Œæ¸…空会导致调度器加锁报错)
    -- æ¸…理后如需恢复保留任务的调度注册,在【基础设施-定时任务】页面点"同步"或调用
    -- POST /admin-api/infra/job/sync ä»Ž infra_job é‡å»ºè§¦å‘器
    TRUNCATE TABLE qrtz_blob_triggers;
    TRUNCATE TABLE qrtz_calendars;
    TRUNCATE TABLE qrtz_cron_triggers;
    TRUNCATE TABLE qrtz_fired_triggers;
    TRUNCATE TABLE qrtz_job_details;
    TRUNCATE TABLE qrtz_paused_trigger_grps;
    TRUNCATE TABLE qrtz_scheduler_state;
    TRUNCATE TABLE qrtz_simple_triggers;
    TRUNCATE TABLE qrtz_simprop_triggers;
    TRUNCATE TABLE qrtz_triggers;
    -- ============================================================
    -- 2. æ–‡ä»¶ä¸Žé™„ä»¶
    -- ============================================================
    TRUNCATE TABLE system_storage_attachment;
    TRUNCATE TABLE system_storage_blob;
    TRUNCATE TABLE storage_attachment;
    TRUNCATE TABLE storage_blob;
    TRUNCATE TABLE infra_file;
    TRUNCATE TABLE infra_file_content;
    -- ============================================================
    -- 3. BPM æµç¨‹å®žä¾‹ä¸Žå®¡æ‰¹åŽ†å²
    --    ä¿ç•™ï¼šæµç¨‹å®šä¹‰/表单/分类/表达式/监听器/用户组/业务附件配置
    --    ä¿ç•™ï¼šact_re_* / act_ge_* / act_id_* / act_procdef_info(定义与部署资源)
    -- ============================================================
    TRUNCATE TABLE bpm_oa_leave;
    TRUNCATE TABLE bpm_process_instance_copy;
    TRUNCATE TABLE act_hi_procinst;
    TRUNCATE TABLE act_hi_taskinst;
    TRUNCATE TABLE act_hi_actinst;
    TRUNCATE TABLE act_hi_varinst;
    TRUNCATE TABLE act_hi_identitylink;
    TRUNCATE TABLE act_hi_comment;
    TRUNCATE TABLE act_hi_detail;
    TRUNCATE TABLE act_hi_attachment;
    TRUNCATE TABLE act_hi_entitylink;
    TRUNCATE TABLE act_hi_tsk_log;
    TRUNCATE TABLE act_ru_task;
    TRUNCATE TABLE act_ru_execution;
    TRUNCATE TABLE act_ru_actinst;
    TRUNCATE TABLE act_ru_variable;
    TRUNCATE TABLE act_ru_identitylink;
    TRUNCATE TABLE act_ru_job;
    TRUNCATE TABLE act_ru_timer_job;
    TRUNCATE TABLE act_ru_deadletter_job;
    TRUNCATE TABLE act_ru_suspended_job;
    TRUNCATE TABLE act_ru_external_job;
    TRUNCATE TABLE act_ru_history_job;
    TRUNCATE TABLE act_ru_event_subscr;
    TRUNCATE TABLE act_ru_entitylink;
    TRUNCATE TABLE act_evt_log;
    -- Flowable æ‰¹æ¬¡è¿è¡Œæ—¶ï¼ˆmulti-instance æ‰¹å¤„理,原脚本遗漏)
    TRUNCATE TABLE flw_ru_batch;
    TRUNCATE TABLE flw_ru_batch_part;
    -- ============================================================
    -- 4. MES ç”Ÿäº§æ¨¡å— â€” ä¸šåŠ¡æ•°æ®
    --    ä¿ç•™ï¼šmes_pro_process / mes_pro_route*(工艺与路线定义)
    -- ============================================================
    TRUNCATE TABLE mes_pro_work_order_param_record_detail;
    TRUNCATE TABLE mes_pro_work_order_param_record;
    TRUNCATE TABLE mes_pro_work_order_process;
    TRUNCATE TABLE mes_pro_work_order_bom;
    TRUNCATE TABLE mes_pro_work_order;
    TRUNCATE TABLE mes_pro_task_issue;
    TRUNCATE TABLE mes_pro_task;
    TRUNCATE TABLE mes_pro_task_pause_record;      -- ç”Ÿäº§ä»»åŠ¡åœå·¥è®°å½•ï¼ˆåŽŸè„šæœ¬é—æ¼ï¼‰
    TRUNCATE TABLE mes_pro_feedback;
    TRUNCATE TABLE mes_pro_mps_merge_item;
    TRUNCATE TABLE mes_pro_mps;
    TRUNCATE TABLE mes_pro_card_process;
    TRUNCATE TABLE mes_pro_card;
    TRUNCATE TABLE mes_pro_work_record_log;
    TRUNCATE TABLE mes_pro_work_record;
    TRUNCATE TABLE mes_pro_andon_record;
    TRUNCATE TABLE mes_pro_scan_event;             -- è¢‹ç æ‰«ç å…¥åº“事件(原脚本遗漏)
    -- ============================================================
    -- 5. MES ä»“储模块 â€” ä¸šåŠ¡æ•°æ®
    --    ä¿ç•™ï¼šmes_wm_warehouse / _area / _location / _barcode_config(仓储配置)
    -- ============================================================
    TRUNCATE TABLE mes_wm_transaction;
    TRUNCATE TABLE mes_wm_stock_reserve;
    TRUNCATE TABLE mes_wm_material_stock;
    TRUNCATE TABLE mes_wm_material_stock_bag;      -- åº“存袋码明细(原脚本遗漏)
    TRUNCATE TABLE mes_wm_stock_warning;           -- åº“存预警记录(原脚本遗漏)
    TRUNCATE TABLE mes_wm_sn;
    TRUNCATE TABLE mes_wm_batch;
    TRUNCATE TABLE mes_wm_barcode;
    TRUNCATE TABLE mes_wm_stock_taking_task_result;
    TRUNCATE TABLE mes_wm_stock_taking_task_line;
    TRUNCATE TABLE mes_wm_stock_taking_task;
    TRUNCATE TABLE mes_wm_stock_taking_plan_param;
    TRUNCATE TABLE mes_wm_stock_taking_plan;
    TRUNCATE TABLE mes_wm_package_line;
    TRUNCATE TABLE mes_wm_package;
    TRUNCATE TABLE mes_wm_item_receipt_detail;
    TRUNCATE TABLE mes_wm_item_receipt_line;
    TRUNCATE TABLE mes_wm_item_receipt;
    TRUNCATE TABLE mes_wm_item_consume_detail;
    TRUNCATE TABLE mes_wm_item_consume_line;
    TRUNCATE TABLE mes_wm_item_consume;
    TRUNCATE TABLE mes_wm_product_produce_detail;
    TRUNCATE TABLE mes_wm_product_produce_line;
    TRUNCATE TABLE mes_wm_product_produce;
    TRUNCATE TABLE mes_wm_product_issue_detail;
    TRUNCATE TABLE mes_wm_product_issue_line;
    TRUNCATE TABLE mes_wm_product_issue;
    TRUNCATE TABLE mes_wm_product_receipt_detail;
    TRUNCATE TABLE mes_wm_product_receipt_line;
    TRUNCATE TABLE mes_wm_product_receipt;
    TRUNCATE TABLE mes_wm_product_sales_detail;
    TRUNCATE TABLE mes_wm_product_sales_line;
    TRUNCATE TABLE mes_wm_product_sales;
    TRUNCATE TABLE mes_wm_misc_receipt_detail;
    TRUNCATE TABLE mes_wm_misc_receipt_line;
    TRUNCATE TABLE mes_wm_misc_receipt;
    TRUNCATE TABLE mes_wm_misc_issue_detail;
    TRUNCATE TABLE mes_wm_misc_issue_line;
    TRUNCATE TABLE mes_wm_misc_issue;
    TRUNCATE TABLE mes_wm_outsource_receipt_detail;
    TRUNCATE TABLE mes_wm_outsource_receipt_line;
    TRUNCATE TABLE mes_wm_outsource_receipt;
    TRUNCATE TABLE mes_wm_outsource_issue_detail;
    TRUNCATE TABLE mes_wm_outsource_issue_line;
    TRUNCATE TABLE mes_wm_outsource_issue;
    TRUNCATE TABLE mes_wm_return_vendor_detail;
    TRUNCATE TABLE mes_wm_return_vendor_line;
    TRUNCATE TABLE mes_wm_return_vendor;
    TRUNCATE TABLE mes_wm_return_issue_detail;
    TRUNCATE TABLE mes_wm_return_issue_line;
    TRUNCATE TABLE mes_wm_return_issue;
    TRUNCATE TABLE mes_wm_return_sales_detail;
    TRUNCATE TABLE mes_wm_return_sales_line;
    TRUNCATE TABLE mes_wm_return_sales;
    TRUNCATE TABLE mes_wm_sales_notice_line;
    TRUNCATE TABLE mes_wm_sales_notice;
    TRUNCATE TABLE mes_wm_transfer_detail;
    TRUNCATE TABLE mes_wm_transfer_line;
    TRUNCATE TABLE mes_wm_transfer;
    TRUNCATE TABLE mes_wm_arrival_notice_line;
    TRUNCATE TABLE mes_wm_arrival_notice;
    -- ============================================================
    -- 6. MES è´¨æ£€æ¨¡å— â€” ä¸šåŠ¡æ•°æ®
    --    ä¿ç•™ï¼šmes_qc_template(_indicator/_item) / _indicator / _defect(质检配置与字典)
    -- ============================================================
    TRUNCATE TABLE mes_qc_indicator_result_detail;
    TRUNCATE TABLE mes_qc_indicator_result;
    TRUNCATE TABLE mes_qc_defect_record;
    TRUNCATE TABLE mes_qc_ncr;
    TRUNCATE TABLE mes_qc_iqc_line;
    TRUNCATE TABLE mes_qc_iqc;
    TRUNCATE TABLE mes_qc_ipqc_line;
    TRUNCATE TABLE mes_qc_ipqc;
    TRUNCATE TABLE mes_qc_oqc_line;
    TRUNCATE TABLE mes_qc_oqc;
    TRUNCATE TABLE mes_qc_rqc_line;
    TRUNCATE TABLE mes_qc_rqc;
    -- ç§å­è´¨æ£€è®°å½•(原脚本遗漏)
    TRUNCATE TABLE mes_qc_seed_quality_indicator;
    TRUNCATE TABLE mes_qc_seed_quality_source;
    TRUNCATE TABLE mes_qc_seed_quality;
    -- ============================================================
    -- 7. MES è¿½æº¯æ¨¡å— â€” äº‹ä»¶ä¸Žé¢„警记录(原脚本遗漏)
    --    ä¿ç•™ï¼šmes_trace_anti_config(防伪窜配置)
    -- ============================================================
    TRUNCATE TABLE mes_trace_consumer_scan_event;
    TRUNCATE TABLE mes_trace_cross_region_warning;
    TRUNCATE TABLE mes_trace_integrity_record;
    TRUNCATE TABLE mes_trace_event;
    -- ============================================================
    -- 8. MES è®¾å¤‡æ¨¡å— â€” ç‚¹æ£€/保养/ç»´ä¿®/计量检测记录
    --    ä¿ç•™ï¼šmes_dv_machinery(_type) / _check_plan* / _subject / _metering(台账与计划)
    -- ============================================================
    TRUNCATE TABLE mes_dv_check_record_line;
    TRUNCATE TABLE mes_dv_check_record;
    TRUNCATE TABLE mes_dv_mainten_record_line;
    TRUNCATE TABLE mes_dv_mainten_record;
    TRUNCATE TABLE mes_dv_repair_line;
    TRUNCATE TABLE mes_dv_repair;
    TRUNCATE TABLE mes_dv_metering_check;
    -- ============================================================
    -- 9. MES å·¥è‰º/设计模块 â€” ä¸šåŠ¡æ•°æ®ï¼ˆç¼–ç ç”Ÿæˆè®°å½•ä¸ºæµæ°´æ•°æ®ï¼‰
    --    ä¿ç•™ï¼šmes_pd_process_param(_template/_detail)(工艺参数定义)
    --          / mes_md_auto_code_rule / _part(编码规则)
    -- ============================================================
    TRUNCATE TABLE mes_pd_archive;
    TRUNCATE TABLE mes_pd_document_audit;
    TRUNCATE TABLE mes_pd_document;
    TRUNCATE TABLE mes_pd_project;
    TRUNCATE TABLE mes_md_auto_code_record;
    -- ============================================================
    -- 10. CRM æ¨¡å— â€” ä¸šåŠ¡æ•°æ®
    --     ä¿ç•™ï¼šcrm_customer / crm_contact / crm_business_status(_type)
    --           / crm_contract_config / crm_customer_limit_config / crm_customer_pool_config
    -- ============================================================
    TRUNCATE TABLE crm_receivable_plan;
    TRUNCATE TABLE crm_receivable;
    TRUNCATE TABLE crm_contract_product;
    TRUNCATE TABLE crm_contract;
    TRUNCATE TABLE crm_sale_quotation_product;
    TRUNCATE TABLE crm_sale_quotation;
    TRUNCATE TABLE crm_business_product;
    TRUNCATE TABLE crm_contact_business;
    TRUNCATE TABLE crm_business;
    TRUNCATE TABLE crm_clue;
    TRUNCATE TABLE crm_follow_up_record;
    TRUNCATE TABLE crm_invoice;
    TRUNCATE TABLE crm_sales_target;
    TRUNCATE TABLE crm_permission;
    -- ============================================================
    -- 11. å”®åŽæ¨¡å— â€” ä¸šåŠ¡æ•°æ®
    -- ============================================================
    TRUNCATE TABLE after_sale_return_item;
    TRUNCATE TABLE after_sale_return;
    TRUNCATE TABLE after_sale_ticket_item;
    TRUNCATE TABLE after_sale_ticket;
    TRUNCATE TABLE after_sale_repair;
    TRUNCATE TABLE after_sale_permission;
    -- ============================================================
    -- 12. ERP æ¨¡å— â€” ä¸šåŠ¡æ•°æ®
    --     ä¿ç•™ï¼šerp_supplier / erp_account
    -- ============================================================
    TRUNCATE TABLE erp_finance_payment_item;
    TRUNCATE TABLE erp_finance_payment;
    TRUNCATE TABLE erp_finance_receipt_item;
    TRUNCATE TABLE erp_finance_receipt;
    TRUNCATE TABLE erp_purchase_order_items;
    TRUNCATE TABLE erp_purchase_order;
    TRUNCATE TABLE erp_purchase_request_items;
    TRUNCATE TABLE erp_purchase_request;
    TRUNCATE TABLE erp_sale_order_items;
    TRUNCATE TABLE erp_sale_order;
    TRUNCATE TABLE erp_purchase_invoice;
    -- ============================================================
    -- 13. SRM æ¨¡å— â€” ä¸šåŠ¡æ•°æ®
    --     ä¿ç•™ï¼šsrm_supplier / srm_supplier_contact / srm_supplier_category
    --           / srm_tender_config
    -- ============================================================
    TRUNCATE TABLE srm_bid_award;
    TRUNCATE TABLE srm_bid_evaluation;
    TRUNCATE TABLE srm_bid_open;
    TRUNCATE TABLE srm_tender_bid;
    TRUNCATE TABLE srm_tender_material;
    TRUNCATE TABLE srm_tender_project;
    TRUNCATE TABLE srm_delivery_plan;
    TRUNCATE TABLE srm_purchase_collaboration;
    TRUNCATE TABLE srm_supplier_quote;
    TRUNCATE TABLE srm_supplier_evaluation;
    TRUNCATE TABLE srm_supplier_certificate;
    TRUNCATE TABLE srm_supplier_apply;
    -- ============================================================
    -- 14. HRM æ¨¡å— â€” ä¸šåŠ¡æ•°æ®
    --     ä¿ç•™ï¼šhrm_employee åŠæ¡£æ¡ˆå­è¡¨ï¼ˆæ•™è‚²/工作经历/紧急联系人/
    --           ç¤¾ä¿å…¬ç§¯é‡‘/薪酬档案/合同)、考勤节假日/规则、请假类型配置、
    --           è–ªé…¬ç»“构、社保公积金方案、个税税率配置
    -- ============================================================
    TRUNCATE TABLE hrm_salary_payment_detail;
    TRUNCATE TABLE hrm_salary_payment;
    TRUNCATE TABLE hrm_salary_calculation;
    TRUNCATE TABLE hrm_attendance_summary;
    TRUNCATE TABLE hrm_attendance_record;
    TRUNCATE TABLE hrm_attendance_exception;
    TRUNCATE TABLE hrm_leave_application;
    TRUNCATE TABLE hrm_resignation_application;
    TRUNCATE TABLE hrm_transfer_application;
    -- ============================================================
    -- 15. AI æ¨¡å— â€” å¯¹è¯ä¸Žç”Ÿæˆå†…容
    --     ä¿ç•™ï¼šai_api_key / ai_model / ai_tool / ai_chat_role
    --           / ai_knowledge(_document/_segment) / ai_workflow
    -- ============================================================
    TRUNCATE TABLE ai_chat_message;
    TRUNCATE TABLE ai_chat_conversation;
    TRUNCATE TABLE ai_image;
    TRUNCATE TABLE ai_mind_map;
    TRUNCATE TABLE ai_music;
    TRUNCATE TABLE ai_write;
    -- ============================================================
    -- 16. IM æ¨¡å— â€” ä¼šè¯ä¸Žæ¶ˆæ¯
    --     ä¿ç•™ï¼šim_sensitive_word / im_face_pack / im_face_pack_item
    -- ============================================================
    TRUNCATE TABLE im_group_message_receipt;
    TRUNCATE TABLE im_group_message;
    TRUNCATE TABLE im_private_message;
    TRUNCATE TABLE im_message_file;
    TRUNCATE TABLE im_group_request;
    TRUNCATE TABLE im_group_member;
    TRUNCATE TABLE im_group;
    TRUNCATE TABLE im_friend_request;
    TRUNCATE TABLE im_friend;
    TRUNCATE TABLE im_conversation_read;
    TRUNCATE TABLE im_conversation;
    TRUNCATE TABLE im_channel_message;
    TRUNCATE TABLE im_channel_material;
    TRUNCATE TABLE im_channel;
    TRUNCATE TABLE im_rtc_participant;
    TRUNCATE TABLE im_rtc_call;
    TRUNCATE TABLE im_user_online;
    TRUNCATE TABLE im_face_user_item;
    -- ============================================================
    -- 17. OA å·²è¯»/指定人关联(保留通知公告内容 oa_notice / system_notice)
    -- ============================================================
    TRUNCATE TABLE oa_notice_read;
    TRUNCATE TABLE oa_notice_user;
    -- ============================================================
    -- 18. ç¤ºä¾‹æ•°æ®
    -- ============================================================
    TRUNCATE TABLE yudao_demo01_contact;
    TRUNCATE TABLE yudao_demo02_category;
    TRUNCATE TABLE yudao_demo03_course;
    TRUNCATE TABLE yudao_demo03_grade;
    TRUNCATE TABLE yudao_demo03_student;
    -- ============================================================
    -- 19. åŸºç¡€æ•°æ®ä¸Žä¸€è¢‹ä¸€ç ç®¡ç†
    --     åŸºç¡€æ•°æ®ï¼šç‰©æ–™ç®¡ç† / ç‰©æ–™åˆ†ç±» / è®¡é‡å•位 / å“ç‰Œ / SKU
    --     ä¸€è¢‹ä¸€ç ï¼šäº§çº¿ç»´æŠ¤ / æ‰¹æ¬¡ç”Ÿäº§ç®¡ç† / èµ‹ç ç®¡ç† / æ‰˜ç›˜ç ç®¡ç†
    --     å“ç§æ•°æ®æºï¼šmes_md_item(与 mdm_item åŒæ­¥ï¼Œæ‰¹æ¬¡/袋码选品种使用)
    --     ä¿ç•™ï¼šmes_md_vendor(供应商,与 erp_supplier ç­–略一致)
    --           / mdm_warehouse(仓库,与 mes_wm_warehouse ç­–略一致)
    --           / mes_md_workshop / mes_md_workstation*(车间工作站配置)
    -- ============================================================
    -- ä¸€è¢‹ä¸€ç ï¼šå…ˆæ¸…引用批次的明细,再清批次与产线
    TRUNCATE TABLE mes_pro_batch_worker;
    TRUNCATE TABLE mes_pro_bag_code_replace;        -- è¢‹ç æ›¿æ¢è®°å½•(原脚本遗漏)
    TRUNCATE TABLE mes_pro_bag_code;
    TRUNCATE TABLE mes_pro_pallet;
    TRUNCATE TABLE mes_pro_batch;
    TRUNCATE TABLE mes_pro_line;
    -- å“ç§æ•°æ®æºï¼ˆMES ç‰©æ–™äº§å“å…³è”配置)
    TRUNCATE TABLE mes_md_product_bom;
    TRUNCATE TABLE mes_md_product_sop;
    TRUNCATE TABLE mes_md_product_sip;
    TRUNCATE TABLE mes_md_item_batch_config;
    TRUNCATE TABLE mes_md_unit_measure;             -- MES è®¡é‡å•位(原脚本遗漏)
    TRUNCATE TABLE mes_md_item;
    -- åŸºç¡€æ•°æ®ï¼ˆMDM ç‰©æ–™ä¸»æ•°æ® / åˆ†ç±» / è®¡é‡å•位 / å“ç‰Œ / SKU)
    TRUNCATE TABLE mdm_item_batch_config;
    TRUNCATE TABLE mdm_item_sku;                    -- ç‰©æ–™ SKU(mdm_item å­è¡¨ï¼ŒåŽŸè„šæœ¬é—æ¼ï¼‰
    TRUNCATE TABLE mdm_item;
    TRUNCATE TABLE mdm_item_category;               -- ç‰©æ–™åˆ†ç±»ï¼ˆåŽŸè„šæœ¬é—æ¼ï¼‰
    TRUNCATE TABLE mdm_brand;                       -- å“ç‰Œï¼ˆåŽŸè„šæœ¬é—æ¼ï¼‰
    TRUNCATE TABLE mes_md_item_type;
    TRUNCATE TABLE mdm_unit_measure;
    -- ç‰©æ–™è¿ç§»æ˜ å°„表:如清理后需从 ERP é‡æ–°è¿ç§»ç‰©æ–™ï¼Œè¯·æ”¾å¼€ä¸‹é¢ä¸€è¡Œ
    -- TRUNCATE TABLE mdm_item_migration_map;
    SET FOREIGN_KEY_CHECKS = 1;
    -- ============================================================
    -- æ¸…理结果校验(代表表行数应全部为 0)
    -- ============================================================
    SELECT
        (SELECT COUNT(*) FROM mes_pro_work_order)   AS work_order_cnt,
        (SELECT COUNT(*) FROM mes_wm_material_stock) AS stock_cnt,
        (SELECT COUNT(*) FROM mes_qc_iqc)           AS iqc_cnt,
        (SELECT COUNT(*) FROM crm_contract)         AS contract_cnt,
        (SELECT COUNT(*) FROM erp_sale_order)       AS sale_order_cnt,
        (SELECT COUNT(*) FROM mdm_item)             AS mdm_item_cnt,
        (SELECT COUNT(*) FROM system_operate_log)   AS operate_log_cnt;
    SELECT '业务数据清理完成' AS result;
END $$
DELIMITER ;
sql/mysql/wls_sales_outbound_menu_extract.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
-- =============================================
-- å‡ºåº“流通管理菜单提取脚本
-- å°†"销售仓库"(602479) æå–为一级菜单"出库流通管理"
-- åŽŸç»“æž„ï¼šä»“å‚¨ç‰©æµ(5780) -> é”€å”®ä»“库(602479) -> å‘货通知/销售出库/销售退货
-- =============================================
-- 1. æå–为一级菜单
-- è¯´æ˜Žï¼šé¡¶çº§"营销管理"(2617) å·²å ç”¨ path=sale,故改用 sales-outbound é¿å…è·¯ç”±å†²çª
UPDATE system_menu SET
  parent_id = 0,
  name = '出库流通管理',
  path = 'sales-outbound',
  sort = 10  -- æ”¾åœ¨ä»“储物流(sort=9)之后
WHERE id = 602479 AND deleted = b'0';
-- =============================================
-- è¯´æ˜Žï¼š
-- 1. å­èœå•(发货通知 5867 / é”€å”®å‡ºåº“ 5970 / é”€å”®é€€è´§ 5847) æ— éœ€æ”¹åŠ¨ï¼Œ
--    å‰ç«¯åŠ¨æ€è·¯ç”±ä¼šè‡ªåŠ¨æ‹¼æŽ¥ä¸º /sales-outbound/sales-notice ç­‰è·¯å¾„
-- 2. é¡¶çº§ç›®å½•菜单由前端自动渲染为 BasicLayout,component å­—段无需填写
-- 3. å¦‚需恢复,执行以下反向脚本:
-- =============================================
-- åå‘脚本(如需恢复):
-- UPDATE system_menu SET parent_id = 5780, name = '销售仓库', path = 'sale', sort = 5 WHERE id = 602479;
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/BizNoUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.framework.common.util.number;
import java.util.concurrent.ThreadLocalRandom;
/**
 * ä¸šåŠ¡ç¼–ç ï¼ˆå•æ®å· / ä¸»æ•°æ®ç¼–码)生成的公共约定。
 *
 * <p>需求:系统中所有自动生成的业务编码统一为「16 ä½çº¯æ•°å­—」,且**随机生成、不使用自增/顺序**,
 * åŒæ—¶ä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä»»æ„ä¸¤æ¡ç¼–码都不重复)。
 *
 * <p>实现约定:
 * <ul>
 *     <li>编码 = éšæœº 16 ä½æ•°å­—(范围 0 ~ 10^16-1,左侧补 '0');</li>
 *     <li>唯一性由 Redis é›†åˆ {@link #USED_SET_KEY} æŸ¥é‡ä¿è¯ï¼šéšæœºç”ŸæˆåŽå†™å…¥é›†åˆï¼Œ
 *         å†™æˆåŠŸï¼ˆè¿”å›ž 1)说明没用过才采用,否则重试;</li>
 *     <li>各生成点:见 {@link #randomCode()} ä¸Ž {@link #MAX_RETRY}。</li>
 * </ul>
 *
 * <p>注意:Redis é›†åˆä¸¢å¤±åŽï¼ŒåŽ†å²ç¼–ç å¯èƒ½è¢«é‡æ–°éšæœºå‘½ä¸­ï¼›åº”ç”¨å¯åŠ¨æ—¶ä¼šå°†åº“å†…å·²æœ‰ç¼–ç 
 * é‡æ–°è½½å…¥é›†åˆï¼ˆè§ yudao-server çš„启动兜底)。
 */
public class BizNoUtils {
    /**
     * å·²ä½¿ç”¨ä¸šåŠ¡ç¼–ç çš„ Redis é›†åˆ KEY,用于随机编码查重,保证全系统唯一。
     */
    public static final String USED_SET_KEY = "biz:code:used";
    /**
     * ä¸šåŠ¡ç¼–ç çš„æ€»é•¿åº¦ï¼š16 ä½
     */
    public static final int CODE_LENGTH = 16;
    /**
     * éšæœºç¼–码的空间上限(不含):10^16
     */
    private static final long CODE_SPACE = 10_000_000_000_000_000L;
    /**
     * ç”Ÿæˆéšæœºç¼–码时的最大重试次数(命中已用编码时重试)
     */
    public static final int MAX_RETRY = 10;
    /**
     * ç”Ÿæˆä¸€ä¸ªéšæœº 16 ä½çº¯æ•°å­—编码(左侧补 '0'),**不做查重**。
     *
     * <p>调用方需自行用 {@link #USED_SET_KEY} æŸ¥é‡ï¼Œå‘½ä¸­åˆ™é‡è¯•。
     *
     * @return éšæœº 16 ä½çº¯æ•°å­—编码
     */
    public static String randomCode() {
        return String.format("%0" + CODE_LENGTH + "d", ThreadLocalRandom.current().nextLong(CODE_SPACE));
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/dal/redis/no/AfterSaleNoRedisDAO.java
@@ -1,14 +1,9 @@
package cn.iocoder.yudao.module.aftersales.dal.redis.no;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.module.aftersales.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
 * å”®åŽæ¨¡å—单号 Redis DAO
@@ -30,17 +25,24 @@
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆåºå·ï¼Œä½¿ç”¨å½“前日期,格式为 {PREFIX} + yyyyMMdd + 6 ä½è‡ªå¢ž
     * ç”Ÿæˆ 16 ä½çº¯æ•°å­—业务编码(全系统唯一)。
     *
     * @param prefix å‰ç¼€
     * @return åºå·
     * <p>为兼容既有调用方,保留 {@code prefix} å…¥å‚,但不再参与编码拼接。
     *
     * @param prefix å•据类型前缀(已不再参与编码拼接)
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate(String prefix) {
        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
        String key = RedisKeyConstants.NO + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        stringRedisTemplate.expire(key, Duration.ofDays(1L));
        return noPrefix + String.format("%06d", no);
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java
@@ -130,8 +130,6 @@
        MesWmReturnSalesCreateReqDTO mesReqDTO = new MesWmReturnSalesCreateReqDTO();
        mesReqDTO.setCode(returnDO.getNo());
        mesReqDTO.setName("售后退货-" + returnDO.getNo());
        mesReqDTO.setSaleOrderId(returnDO.getSaleOrderId());
        mesReqDTO.setSalesOrderCode(ticket != null ? ticket.getSaleOrderNo() : null);
        mesReqDTO.setClientId(returnDO.getCustomerId());
        mesReqDTO.setReturnDate(LocalDate.now());
        mesReqDTO.setReturnReason(returnDO.getReturnReason());
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java
@@ -1,16 +1,10 @@
package cn.iocoder.yudao.module.bpm.dal.redis;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
import static cn.hutool.core.date.DatePattern.*;
/**
 * BPM æµç¨‹ Id ç¼–码的 Redis DAO
@@ -24,38 +18,24 @@
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆåºå·ï¼Œä½¿ç”¨å®šä¹‰çš„ processIdRule è§„则生成
     * ç”Ÿæˆ 16 ä½çº¯æ•°å­—业务编码(全系统唯一)。
     *
     * @param processIdRule è§„则
     * @return åºå·
     * <p>为兼容既有调用方,保留 {@code processIdRule} å…¥å‚,但不再参与编码拼接。
     *
     * @param processIdRule æµç¨‹ Id è§„则(已不再参与编码拼接)
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate(BpmModelMetaInfoVO.ProcessIdRule processIdRule) {
        // ç”Ÿæˆæ—¥æœŸå‰ç¼€
        String infix = "";
        switch (processIdRule.getInfix()) {
            case "DAY":
                infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN);
                break;
            case "HOUR":
                infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN + "HH");
                break;
            case "MINUTE":
                infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN + "HHmm");
                break;
            case "SECOND":
                infix = DateUtil.format(LocalDateTime.now(), PURE_DATETIME_PATTERN);
                break;
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        // ç”Ÿæˆåºå·
        String noPrefix = processIdRule.getPrefix() + infix + processIdRule.getPostfix();
        String key = RedisKeyConstants.BPM_PROCESS_ID + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        if (StrUtil.isEmpty(infix)) {
            // ç‰¹æ®Šï¼šæ²¡æœ‰å‰ç¼€ï¼Œåˆ™ä¸èƒ½è¿‡æœŸï¼Œä¸èƒ½æ¯æ¬¡éƒ½æ˜¯ä»Ž 0 å¼€å§‹ã€‚可见 https://t.zsxq.com/MU1E2 è®¨è®º
            stringRedisTemplate.expire(key, Duration.ofDays(1L));
        }
        return noPrefix + String.format("%0" + processIdRule.getLength() + "d", no);
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java
@@ -1,14 +1,9 @@
package cn.iocoder.yudao.module.crm.dal.redis.no;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.module.crm.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
@@ -43,20 +38,24 @@
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆåºå·ï¼Œä½¿ç”¨å½“前日期,格式为 {PREFIX} + yyyyMMdd + 6 ä½è‡ªå¢ž
     * ä¾‹å¦‚说:QTRK 202109 000001 ï¼ˆæ²¡æœ‰ä¸­é—´ç©ºæ ¼ï¼‰
     * ç”Ÿæˆ 16 ä½çº¯æ•°å­—业务编码(全系统唯一)。
     *
     * @param prefix å‰ç¼€
     * @return åºå·
     * <p>为兼容既有调用方,保留 {@code prefix} å…¥å‚,但不再参与编码拼接。
     *
     * @param prefix å•据类型前缀(已不再参与编码拼接)
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate(String prefix) {
        // é€’增序号
        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
        String key = RedisKeyConstants.NO + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        // è®¾ç½®è¿‡æœŸæ—¶é—´
        stringRedisTemplate.expire(key, Duration.ofDays(1L));
        return noPrefix + String.format("%06d", no);
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -21,9 +21,6 @@
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.api.productsales.MesWmProductSalesLineApi;
import cn.iocoder.yudao.module.mes.api.salesnotice.MesWmSalesNoticeApi;
import cn.iocoder.yudao.module.mes.api.wms.MesWmStockApi;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -63,12 +60,6 @@
    private MdmItemApi mdmItemApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesWmSalesNoticeApi salesNoticeApi;
    @Resource
    private MesWmProductSalesLineApi productSalesLineApi;
    @Resource
    private MesProMpsApi mpsApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
@@ -155,14 +146,11 @@
        // 2. æ‰¹é‡æŸ¥è¯¢å…³è”数据
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getProductId));
        Map<Long, String> batchCodeMap = productSalesLineApi.getBatchCodesBySaleOrderItemIds(
                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getId));
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getProductUnitId));
        // 3. è½¬æ¢è®¢å•项
        List<ErpSaleOrderRespVO.Item> itemVOList = BeanUtils.toBean(saleOrderItemList, ErpSaleOrderRespVO.Item.class, item -> {
            item.setStockCount(mesWmStockApi.getStockQuantity(item.getProductId()));
            item.setBatchCode(batchCodeMap.get(item.getId()));
            MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem ->
                    item.setProductName(mdmItem.getName()).setProductBarCode(mdmItem.getBarCode())
                            .setProductSpecification(mdmItem.getSpecification()));
@@ -213,54 +201,13 @@
        // 1.4 ç®¡ç†å‘˜ä¿¡æ¯
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(pageResult.getList(), saleOrder -> Long.parseLong(saleOrder.getCreator())));
        // 1.5 å‘货通知单信息
        Map<Long, Boolean> salesNoticeMap = salesNoticeApi.checkSalesNoticeBySaleOrderIds(
                convertSet(pageResult.getList(), ErpSaleOrderDO::getId));
        // 1.6 å‡ºåº“批号
        Map<Long, String> batchCodeMap = productSalesLineApi.getBatchCodesBySaleOrderItemIds(
                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getId));
        // 1.7 è®¡é‡å•位
        // 1.5 è®¡é‡å•位
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getProductUnitId));
        // 1.8 æ£€æŸ¥æ˜¯å¦å¯ä»¥ç”Ÿæˆå‘货通知单(需要生产的明细对应的 MPS æ˜¯å¦å·²å®Œæˆï¼‰
        // ç­›é€‰å‡ºéœ€è¦ç”Ÿäº§çš„æ˜Žç»†ID
        List<Long> needProductionItemIds = saleOrderItemList.stream()
                .filter(item -> item.getNeedProduction() != null && item.getNeedProduction() == 1)
                .map(ErpSaleOrderItemDO::getId)
                .toList();
        Map<Long, Boolean> canCreateSalesNoticeMap = new java.util.HashMap<>();
        if (!needProductionItemIds.isEmpty()) {
            // æŒ‰è®¢å•分组明细ID
            Map<Long, List<Long>> orderItemIdsMap = new java.util.HashMap<>();
            for (ErpSaleOrderItemDO item : saleOrderItemList) {
                orderItemIdsMap.computeIfAbsent(item.getOrderId(), k -> new java.util.ArrayList<>())
                        .add(item.getId());
            }
            // æ£€æŸ¥æ¯ä¸ªè®¢å•
            for (Map.Entry<Long, List<Long>> entry : orderItemIdsMap.entrySet()) {
                List<Long> itemIds = entry.getValue().stream()
                        .filter(id -> {
                            ErpSaleOrderItemDO item = saleOrderItemList.stream()
                                    .filter(i -> i.getId().equals(id))
                                    .findFirst().orElse(null);
                            return item != null && item.getNeedProduction() != null && item.getNeedProduction() == 1;
                        }
                ).toList();
                if (itemIds.isEmpty()) {
                    canCreateSalesNoticeMap.put(entry.getKey(), true);
                } else {
                    canCreateSalesNoticeMap.put(entry.getKey(), mpsApi.checkCanCreateSalesNotice(itemIds));
                }
            }
        } else {
            // æ²¡æœ‰éœ€è¦ç”Ÿäº§çš„æ˜Žç»†ï¼Œéƒ½å¯ä»¥ç”Ÿæˆå‘货通知单
            pageResult.getList().forEach(order -> canCreateSalesNoticeMap.put(order.getId(), true));
        }
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, ErpSaleOrderRespVO.class, saleOrder -> {
            saleOrder.setItems(BeanUtils.toBean(saleOrderItemMap.get(saleOrder.getId()), ErpSaleOrderRespVO.Item.class,
                    item -> {
                        item.setBatchCode(batchCodeMap.get(item.getId()));
                        MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem ->
                                item.setProductName(mdmItem.getName()).setProductBarCode(mdmItem.getBarCode())
                                        .setProductSpecification(mdmItem.getSpecification()));
@@ -270,10 +217,6 @@
            saleOrder.setProductNames(CollUtil.join(saleOrder.getItems(), ",", ErpSaleOrderRespVO.Item::getProductName));
            MapUtils.findAndThen(customerMap, saleOrder.getCustomerId(), supplier -> saleOrder.setCustomerName(supplier.getName()));
            MapUtils.findAndThen(userMap, Long.parseLong(saleOrder.getCreator()), user -> saleOrder.setCreatorName(user.getNickname()));
            // è®¾ç½®æ˜¯å¦å·²ç”Ÿæˆå‘货通知单
            saleOrder.setHasSalesNotice(salesNoticeMap.getOrDefault(saleOrder.getId(), false));
            // è®¾ç½®æ˜¯å¦å¯ä»¥ç”Ÿæˆå‘货通知单:已生成发货通知单时不显示按钮
            saleOrder.setCanCreateSalesNotice(!saleOrder.getHasSalesNotice() && canCreateSalesNoticeMap.getOrDefault(saleOrder.getId(), true));
            // è®¾ç½®æ˜¯å¦æœ‰éœ€è¦ç”Ÿäº§çš„æ˜Žç»†
            boolean hasNeedProduction = saleOrder.getItems().stream()
                    .anyMatch(item -> item.getNeedProduction() != null && item.getNeedProduction() == 1);
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -104,18 +104,10 @@
    @Schema(description = "出库状态", example = "0")
    private Integer outStatus;
    // ========== å‘货通知 ==========
    @Schema(description = "是否已生成发货通知单", example = "false")
    private Boolean hasSalesNotice;
    @Schema(description = "是否可以生成发货通知单(有需要生产且未完成的明细时为 false)", example = "true")
    private Boolean canCreateSalesNotice;
    // ========== ç”Ÿäº§å…³è” ==========
    @Schema(description = "是否有需要生产的明细(0=否 1=是)", example = "1")
    private Integer needProduction;
    // ========== ç”Ÿäº§å…³è” ==========
    @Schema(description = "生产状态:0-未完成,1-已完成", example = "1")
    private Integer productionStatus;
@@ -163,11 +155,6 @@
        @Schema(description = "关联的 MPS ç¼–号", example = "1024")
        private Long mpsId;
        // ========== å‡ºåº“批号 ==========
        @Schema(description = "出库批号", example = "B2026001、B2026002")
        private String batchCode;
        // ========== å…³è”字段 ==========
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
@@ -1,14 +1,9 @@
package cn.iocoder.yudao.module.erp.dal.redis.no;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.module.erp.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
@@ -52,20 +47,24 @@
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆåºå·ï¼Œä½¿ç”¨å½“前日期,格式为 {PREFIX} + yyyyMMdd + 6 ä½è‡ªå¢ž
     * ä¾‹å¦‚说:CGDD 202109 000001 ï¼ˆæ²¡æœ‰ä¸­é—´ç©ºæ ¼ï¼‰
     * ç”Ÿæˆ 16 ä½çº¯æ•°å­—业务编码(全系统唯一)。
     *
     * @param prefix å‰ç¼€
     * @return åºå·
     * <p>为兼容既有调用方,保留 {@code prefix} å…¥å‚,但不再参与编码拼接。
     *
     * @param prefix å•据类型前缀(已不再参与编码拼接)
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate(String prefix) {
        // é€’增序号
        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
        String key = RedisKeyConstants.NO + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        // è®¾ç½®è¿‡æœŸæ—¶é—´
        stringRedisTemplate.expire(key, Duration.ofDays(1L));
        return noPrefix + String.format("%06d", no);
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
@@ -1,13 +1,9 @@
package cn.iocoder.yudao.module.hrm.dal.redis;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
 * HRM ç¼–号生成的 Redis DAO
@@ -131,19 +127,23 @@
    }
    /**
     * ç”Ÿæˆç¼–号
     * ç”Ÿæˆ 16 ä½çº¯æ•°å­—业务编码(全系统唯一)。
     *
     * @param prefix å‰ç¼€
     * @param digits ä½æ•°
     * @return ç¼–号
     * <p>为兼容既有调用方,保留入参,但不再参与编码拼接。
     *
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate(String prefix, int digits) {
        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
        String key = HrmRedisKeyConstants.NO + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        // è®¾ç½®è¿‡æœŸæ—¶é—´ 1 å¤©
        stringRedisTemplate.expire(key, Duration.ofDays(1L));
        return noPrefix + String.format("%0" + digits + "d", no);
        // å…¨ç³»ç»Ÿå…±ç”¨åŒä¸€è‡ªå¢žåºåˆ—,保证编码「16 ä½çº¯æ•°å­—」且全系统唯一
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
}
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/unit/dto/MdmUnitMeasureRespDTO.java
@@ -18,6 +18,9 @@
    @Schema(description = "单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "个")
    private String name;
    @Schema(description = "类型(48千克/100千克/吨)", example = "48千克")
    private String type;
    @Schema(description = "是否主单位", example = "true")
    private Boolean primaryFlag;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java
@@ -8,12 +8,10 @@
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemStatusUpdateReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSyncUpdateReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.item.MdmItemBatchConfigService;
import cn.iocoder.yudao.module.mdm.service.item.MdmItemService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
@@ -47,8 +45,6 @@
    @Resource
    private MesMdItemApi mesMdItemApi;
    @Resource
    private MdmItemCategoryService itemCategoryService;
    @Resource
    private MdmUnitMeasureService unitMeasureService;
    @Resource
@@ -129,9 +125,6 @@
    @PreAuthorize("@ss.hasPermission('mdm:item:query')")
    public CommonResult<PageResult<MdmItemRespVO>> getItemPage(@Valid MdmItemPageReqVO pageReqVO) {
        PageResult<MdmItemDO> pageResult = itemService.getItemPage(pageReqVO);
        // å…³è”查询分类名称
        Map<Long, MdmItemCategoryDO> categoryMap = itemCategoryService.getItemCategoryMap(
                convertSet(pageResult.getList(), MdmItemDO::getCategoryId));
        // å…³è”查询计量单位名称
        Map<Long, MdmUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                convertSet(pageResult.getList(), MdmItemDO::getUnitMeasureId));
@@ -158,11 +151,6 @@
                        .collect(Collectors.toMap(MesWmWarehouseRespDTO::getId, w -> w));
        // èµ‹å€¼
        respList.forEach(resp -> {
            // åˆ†ç±»åç§°
            MdmItemCategoryDO category = categoryMap.get(resp.getCategoryId());
            if (category != null) {
                resp.setCategoryName(category.getName());
            }
            MdmUnitMeasureDO unit = unitMap.get(resp.getUnitMeasureId());
            if (unit != null) {
                resp.setUnitMeasureName(unit.getName());
@@ -215,13 +203,6 @@
     * å¡«å……物料响应 VO çš„关联名称(分类、主单位、辅单位、仓库名称)
     */
    private void enrichItemRespVO(MdmItemRespVO resp) {
        // åˆ†ç±»åç§°
        if (resp.getCategoryId() != null) {
            MdmItemCategoryDO category = itemCategoryService.getItemCategory(resp.getCategoryId());
            if (category != null) {
                resp.setCategoryName(category.getName());
            }
        }
        // ä¸»å•位名称
        if (resp.getUnitMeasureId() != null) {
            MdmUnitMeasureDO unit = unitMeasureService.getUnitMeasure(resp.getUnitMeasureId());
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemPageReqVO.java
@@ -23,9 +23,6 @@
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "物料分类编号", example = "1")
    private Long categoryId;
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemRespVO.java
@@ -25,12 +25,6 @@
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long categoryId;
    @Schema(description = "物料分类名称", example = "五金配件")
    private String categoryName;
    @Schema(description = "计量单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long unitMeasureId;
@@ -75,9 +69,6 @@
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java
@@ -24,9 +24,6 @@
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号(关联 MES ç‰©æ–™åˆ†ç±» mes_md_item_type)", example = "1")
    private Long categoryId;
    @Schema(description = "计量单位编号", example = "1")
    private Long unitMeasureId;
@@ -53,9 +50,6 @@
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mdm.controller.admin.unit.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.math.BigDecimal;
@@ -18,6 +19,10 @@
    @Schema(description = "单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "个")
    private String name;
    @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "48千克")
    @NotBlank(message = "类型不能为空")
    private String type;
    @Schema(description = "是否主单位", example = "true")
    private Boolean primaryFlag;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java
@@ -40,6 +40,13 @@
    private String name;
    /**
     * ç±»åž‹
     *
     * å¯é€‰å€¼ï¼š48千克、100千克、吨
     */
    private String type;
    /**
     * æ˜¯å¦ä¸»å•位
     */
    private Boolean primaryFlag;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
@@ -2,28 +2,20 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemBatchConfigMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemApi;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemTypeApi;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemTypeRespDTO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -33,7 +25,6 @@
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mdm.enums.ErrorCodeConstants.*;
/**
@@ -49,8 +40,6 @@
    private MdmItemMapper itemMapper;
    @Resource
    private MdmItemCategoryService itemCategoryService;
    @Resource
    private MdmUnitMeasureService unitMeasureService;
    @Resource
    private MdmBrandService brandService;
@@ -60,9 +49,6 @@
    private MesMdItemApi mesMdItemApi;
    @Resource
    private MdmItemBatchConfigMapper itemBatchConfigMapper;
    @Resource
    private MesMdItemTypeApi mesMdItemTypeApi;
    @Override
@@ -115,10 +101,9 @@
    }
    /**
     * åŒæ­¥ç‰©æ–™åˆ° MES(单位/分类按 code æ˜ å°„)
     * åŒæ­¥ç‰©æ–™åˆ° MES(单位按 code æ˜ å°„)
     *
     * MDM çš„ unitMeasureId/categoryId ä¸Ž MES çš„ unitMeasureId/itemTypeId åˆ†å±žä¸¤å¥—主数据,
     * è¿™é‡Œå…ˆåœ¨ MDM ä¾§è§£æžå‡º code,再交由 MesMdItemApi åœ¨ MES ä¾§è§£æžä¸ºå¯¹åº” id。
     * å“ç§ç®¡ç†å·²åŽ»æŽ‰ã€Œå“ç§åˆ†ç±»ã€å­—æ®µï¼ŒåŒæ­¥ä¸å†ä¼ é€’ MES ç‰©æ–™åˆ†ç±»ï¼ˆitem_type_id ç•™ç©ºï¼Œå¯åœ¨ MES ä¾§è¡¥å¡«ï¼‰ã€‚
     * åŒæ­¥å¤±è´¥å°†æŠ›å‡ºå¼‚常,触发外层 @Transactional æ•´ä½“回滚(MDM æ“ä½œä¸€å¹¶å›žæ»šï¼‰ã€‚
     */
    private void syncItemToMes(MdmItemSaveReqVO reqVO) {
@@ -131,12 +116,6 @@
            }
            unitCode = unitMeasure.getCode();
        }
        // è§£æžåˆ†ç±» id -> code(使用 MDM æœ¬åœ°åˆ†ç±»æœåŠ¡ï¼‰
        String itemTypeCode = null;
        if (reqVO.getCategoryId() != null) {
            CommonResult<MesMdItemTypeRespDTO> itemType = mesMdItemTypeApi.getItemType(reqVO.getCategoryId());
            itemTypeCode = itemType.getData().getCode();
        }
        // ç»„装同步 DTO
        MesMdItemSyncReqDTO syncReqDTO = new MesMdItemSyncReqDTO();
        syncReqDTO.setCode(reqVO.getCode());
@@ -144,7 +123,6 @@
        syncReqDTO.setName(reqVO.getName());
        syncReqDTO.setSpecification(reqVO.getSpecification());
        syncReqDTO.setUnitCode(unitCode);
        syncReqDTO.setItemTypeCode(itemTypeCode);
        syncReqDTO.setSafeStockFlag(Boolean.TRUE); // ä¿ç•™ MDM çš„ minStock/maxStock,避免被 MES æ¸…é›¶
        syncReqDTO.setMinStock(reqVO.getMinStock());
        syncReqDTO.setMaxStock(reqVO.getMaxStock());
@@ -274,17 +252,6 @@
        }
    }
    private void validateCategoryExists(Long categoryId) {
        if (categoryId == null) {
            return;
        }
        // ä½¿ç”¨ MDM æœ¬åœ°åˆ†ç±»æœåŠ¡æ ¡éªŒ
        MdmItemCategoryDO category = itemCategoryService.getItemCategory(categoryId);
        if (category == null) {
            throw exception(MDM_ITEM_CATEGORY_NOT_EXISTS);
        }
    }
    private void validateUnitMeasureExists(Long unitMeasureId) {
        if (unitMeasureService.getUnitMeasure(unitMeasureId) == null) {
            throw exception(MDM_UNIT_MEASURE_NOT_EXISTS);
@@ -328,17 +295,8 @@
    @Override
    public PageResult<MdmItemDO> getItemPage(MdmItemPageReqVO pageReqVO) {
        // æŸ¥æ‰¾æ—¶ï¼Œå¦‚果查找某个分类编号,则包含它的子分类
        Set<Long> categoryIds = null;
        if (pageReqVO.getCategoryId() != null) {
            categoryIds = new HashSet<>();
            categoryIds.add(pageReqVO.getCategoryId());
            List<cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO> children =
                    itemCategoryService.getItemCategoryChildrenList(pageReqVO.getCategoryId());
            categoryIds.addAll(convertSet(children, cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO::getId));
        }
        // åˆ†é¡µæŸ¥è¯¢
        return itemMapper.selectPage(pageReqVO, categoryIds);
        return itemMapper.selectPage(pageReqVO, (Collection<Long>) null);
    }
    @Override
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemSyncReqDTO.java
@@ -21,9 +21,6 @@
    @Schema(description = "计量单位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "PCS")
    private String unitCode;
    @Schema(description = "物料分类编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "TYPE001")
    private String itemTypeCode;
    @Schema(description = "是否启用安全库存", example = "false")
    private Boolean safeStockFlag;
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApi.java
ÎļþÒÑɾ³ý
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesCreateReqDTO.java
@@ -20,12 +20,6 @@
    @Schema(description = "退货单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "售后退货")
    private String name;
    @Schema(description = "销售订单 ID", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单编号", example = "SO202603010001")
    private String salesOrderCode;
    @Schema(description = "客户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long clientId;
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/salesnotice/MesWmSalesNoticeApi.java
ÎļþÒÑɾ³ý
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java
@@ -5,12 +5,10 @@
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.batchconfig.MesMdItemBatchConfigSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -27,7 +25,7 @@
 * MES ç‰©æ–™äº§å“ API å®žçŽ°ç±»
 *
 * ä¾› MDM ç­‰æ¨¡å—同步物料主数据到 MES ä½¿ç”¨ã€‚
 * å•位、分类按 code åœ¨ MES ä¾§è§£æžä¸ºå¯¹åº” id。
 * å•位按 code åœ¨ MES ä¾§è§£æžä¸ºå¯¹åº” id。
 *
 * @author è¶…级管理员
 */
@@ -40,8 +38,6 @@
    private MesMdItemService itemService;
    @Resource
    private MesMdItemMapper itemMapper;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
@@ -92,7 +88,9 @@
    }
    /**
     * å°†åŒæ­¥ DTO è½¬æ¢ä¸º MES ç‰©æ–™äº§å“ SaveReqVO,并按 code è§£æžå•位/分类 id
     * å°†åŒæ­¥ DTO è½¬æ¢ä¸º MES ç‰©æ–™äº§å“ SaveReqVO,并按 code è§£æžå•位 id
     *
     * MDM å“ç§ç®¡ç†å·²åŽ»æŽ‰ã€Œå“ç§åˆ†ç±»ã€ï¼ŒåŒæ­¥ä¸å†è®¾ç½®ç‰©æ–™åˆ†ç±»ï¼ˆitem_type_id ç•™ç©ºï¼Œå¯åœ¨ MES ä¾§æ‰‹å·¥è¡¥å¡«ï¼‰
     */
    private MesMdItemSaveReqVO buildSaveReqVO(MesMdItemSyncReqDTO syncReqDTO) {
        // è§£æžè®¡é‡å•位 code -> id
@@ -100,15 +98,9 @@
        if (unitMeasure == null) {
            throw exception(MD_UNIT_MEASURE_NOT_EXISTS);
        }
        // è§£æžç‰©æ–™åˆ†ç±» code -> id
        MesMdItemTypeDO itemType = itemTypeService.getItemTypeByCode(syncReqDTO.getItemTypeCode());
        if (itemType == null) {
            throw exception(MD_ITEM_TYPE_NOT_EXISTS);
        }
        // ç»„装(code/name/specification/safeStockFlag/minStock/maxStock/highValue/batchFlag/remark åŒåæ‹·è´ï¼‰
        MesMdItemSaveReqVO saveReqVO = BeanUtils.toBean(syncReqDTO, MesMdItemSaveReqVO.class);
        saveReqVO.setUnitMeasureId(unitMeasure.getId());
        saveReqVO.setItemTypeId(itemType.getId());
        return saveReqVO;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/productsales/MesWmProductSalesLineApiImpl.java
ÎļþÒÑɾ³ý
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/salesnotice/MesWmSalesNoticeApiImpl.java
ÎļþÒÑɾ³ý
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/home/vo/MesBagCodeHomeSummaryRespVO.java
@@ -37,6 +37,25 @@
    @Schema(description = "托盘码总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800")
    private Long palletCount;
    // ========== å‡ºåº“流通 ==========
    @Schema(description = "发货通知单数(mes_wm_sales_notice)", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    private Long salesNoticeCount;
    @Schema(description = "销售出库单数(mes_wm_product_sales)", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
    private Long productSalesCount;
    @Schema(description = "销售退货单数(mes_wm_return_sales)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
    private Long returnSalesCount;
    // ========== ä»“储预警 / è´¨é‡ ==========
    @Schema(description = "未处理库存预警数(mes_wm_stock_warning.status=10)", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
    private Long stockWarningPendingCount;
    @Schema(description = "待评审不合格品单数(mes_qc_ncr.status=0)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
    private Long ncrPendingCount;
    // ========== çŠ¶æ€åˆ†å¸ƒ ==========
    @Schema(description = "批次状态分布")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemSaveReqVO.java
@@ -29,8 +29,7 @@
    @NotNull(message = "计量单位不能为空")
    private Long unitMeasureId;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "物料分类编号不能为空")
    @Schema(description = "物料分类编号", example = "1")
    private Long itemTypeId;
    @Schema(description = "是否启用安全库存", example = "false")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/MesProBagCodeController.java
@@ -14,6 +14,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeRawGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeRespVO;
@@ -79,6 +80,14 @@
        return success(bagCodeService.generateBagCodes(generateReqVO));
    }
    @PostMapping("/raw-generate")
    @Operation(summary = "生成原始袋码(仅选品种与袋码数量,不关联计量单位类型、不生成托盘)")
    @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:create')")
    public CommonResult<MesProBagCodeGenerateRespVO> generateRawBagCodes(
            @Valid @RequestBody MesProBagCodeRawGenerateReqVO rawGenerateReqVO) {
        return success(bagCodeService.generateRawBagCodes(rawGenerateReqVO));
    }
    @PostMapping("/replace")
    @Operation(summary = "作废破损袋码并生成等量替换袋码")
    @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:replace')")
@@ -134,7 +143,8 @@
    @GetMapping("/get-by-code")
    @Operation(summary = "扫码追溯:根据袋码(二维码内容)反查批次、产线、责任人等信息")
    @Parameter(name = "code", description = "袋码(二维码内容)", required = true, example = "PC20260818L01001-0001")
    @Parameter(name = "code", description = "单元识别代码(国标:不超过 20 ä¸ªå­—符,数字或字母数字组合)",
            required = true, example = "0286267849370923")
    @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:query')")
    public CommonResult<MesProBagCodeTraceRespVO> getBagCodeByCode(@RequestParam("code") String code) {
        MesProBagCodeDO bagCode = bagCodeService.getBagCodeByCode(code);
@@ -188,11 +198,12 @@
                                HttpServletResponse response) throws IOException {
        List<MesProBagCodeDO> list;
        String zipName;
        // åªå¯¼å‡ºå¯å°åˆ·çš„袋码:已作废袋码的标签不得再印刷(贴到产品上会被扫码提示"已作废,谨防假冒")
        if (StrUtil.isNotEmpty(uuid)) {
            list = bagCodeService.getBagCodeListByUuid(uuid);
            list = bagCodeService.getPrintableBagCodeListByUuid(uuid);
            zipName = "袋码二维码-" + uuid;
        } else if (batchId != null) {
            list = bagCodeService.getBagCodeListByBatchId(batchId);
            list = bagCodeService.getPrintableBagCodeListByBatchId(batchId);
            zipName = "袋码二维码-" + batchId;
        } else {
            throw exception(PRO_QR_EXPORT_PARAMS_INVALID);
@@ -242,8 +253,7 @@
                    .setProduceDate(batch.getProduceDate())
                    .setLineCode(batch.getLineCode())
                    .setLineName(batch.getLineName())
                    .setOwnerUserId(batch.getOwnerUserId())
                    .setItemId(batch.getItemId());
                    .setOwnerUserId(batch.getOwnerUserId());
            // è´£ä»»äºº
            if (batch.getOwnerUserId() != null) {
                AdminUserRespDTO user = adminUserApi.getUser(batch.getOwnerUserId());
@@ -251,14 +261,17 @@
                    respVO.setOwnerUserName(user.getNickname());
                }
            }
            // å“ç§ä¿¡æ¯
            if (batch.getItemId() != null) {
                MesMdmItemDO item = mdmItemService.getItem(batch.getItemId());
                if (item != null) {
                    respVO.setItemCode(item.getCode())
                            .setItemName(item.getName())
                            .setItemSpecification(item.getSpecification());
                }
        }
        // å“ç§ä¿¡æ¯ï¼šä¼˜å…ˆå–袋码上的品种(数据包生成时绑定),历史袋码回退批次品种
        Long itemId = bagCode.getItemId() != null ? bagCode.getItemId()
                : (batch == null ? null : batch.getItemId());
        if (itemId != null) {
            MesMdmItemDO item = mdmItemService.getItem(itemId);
            if (item != null) {
                respVO.setItemId(itemId)
                        .setItemCode(item.getCode())
                        .setItemName(item.getName())
                        .setItemSpecification(item.getSpecification());
            }
        }
        return respVO;
@@ -268,11 +281,12 @@
     * æž„建袋码二维码标签列表(二维码编码纯码值,图片叠加关键信息文字)
     */
    private List<QrLabelInfo> buildBagCodeQrLabels(List<MesProBagCodeDO> list) {
        // 1. èŽ·å¾—å…³è”æ•°æ®ï¼ˆæ‰¹æ¬¡ã€å“ç§ã€è´£ä»»äººï¼‰
        // 1. èŽ·å¾—å…³è”æ•°æ®ï¼ˆæ‰¹æ¬¡ã€å“ç§ã€è´£ä»»äººï¼‰ï¼›å“ç§é›†åˆå«è¢‹ç è‡ªèº«å“ç§ä¸ŽåŽ†å²æ‰¹æ¬¡å“ç§
        Map<Long, MesProBatchDO> batchMap = convertMap(
                batchService.getBatchList(convertSet(list, MesProBagCodeDO::getBatchId)), MesProBatchDO::getId);
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(batchMap.values(), MesProBatchDO::getItemId));
        Set<Long> itemIds = convertSet(list, MesProBagCodeDO::getItemId);
        itemIds.addAll(convertSet(batchMap.values(), MesProBatchDO::getItemId));
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(itemIds);
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(batchMap.values(), MesProBatchDO::getOwnerUserId));
        // 2. æž„建标签
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateReqVO.java
@@ -9,10 +9,17 @@
@Data
public class MesProBagCodeGenerateReqVO {
    @Schema(description = "生成袋数(本次生成的袋码数量)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @NotNull(message = "生成袋数不能为空")
    @Positive(message = "生成袋数必须大于 0")
    private Integer count;
    @Schema(description = "品种(物料)编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "品种不能为空")
    private Long itemId;
    @Schema(description = "托盘数(品种主单位类型为 48千克/100千克 æ—¶å¿…填,袋码数 = æ¯æ‰˜è¢‹æ•° Ã— æ‰˜ç›˜æ•°ï¼‰", example = "5")
    @Positive(message = "托盘数必须大于 0")
    private Integer palletCount;
    @Schema(description = "袋码数(品种主单位类型为 å¨ æ—¶å¿…填,不生成托盘)", example = "100")
    @Positive(message = "袋码数必须大于 0")
    private Integer bagCount;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeGenerateRespVO.java
@@ -3,10 +3,24 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - MES è¢‹ç ç”Ÿæˆå“åº”")
@Data
public class MesProBagCodeGenerateRespVO {
    @Schema(description = "生成组 UUID")
    private String uuid;
    @Schema(description = "品种主单位类型", example = "48千克")
    private String type;
    @Schema(description = "每托盘袋数(类型为 å¨ æ—¶ä¸º 0)", example = "48")
    private Integer perPallet;
    @Schema(description = "本次生成的袋码总数", example = "240")
    private Integer totalCount;
    @Schema(description = "自动生成的托盘编号列表(类型为 å¨ æ—¶ä¸ºç©ºï¼‰")
    private List<Long> palletIds;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodePageReqVO.java
@@ -32,6 +32,9 @@
    @Schema(description = "绑定托盘编号", example = "1024")
    private Long palletId;
    @Schema(description = "品种(物料)编号", example = "1024")
    private Long itemId;
    @Schema(description = "状态(字典 mes_pro_bag_code_status)", example = "10")
    private Integer status;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRawGenerateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
@Schema(description = "管理后台 - MES åŽŸå§‹è¢‹ç ç”Ÿæˆ Request VO(仅品种+数量,不关联计量单位类型、不生成托盘)")
@Data
public class MesProBagCodeRawGenerateReqVO {
    @Schema(description = "品种(物料)编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "品种不能为空")
    private Long itemId;
    @Schema(description = "袋码数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @NotNull(message = "袋码数量不能为空")
    @Positive(message = "袋码数量必须大于 0")
    private Integer count;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/bagcode/vo/MesProBagCodeRespVO.java
@@ -58,6 +58,20 @@
    @ColumnWidth(30)
    private String palletCode;
    @Schema(description = "品种编号", example = "1")
    @ExcelIgnore
    private Long itemId;
    @Schema(description = "品种编码")
    @ExcelProperty("品种编码")
    @ColumnWidth(20)
    private String itemCode;
    @Schema(description = "品种名称")
    @ExcelProperty("品种名称")
    @ColumnWidth(20)
    private String itemName;
    @Schema(description = "状态(字典 mes_pro_bag_code_status)", example = "10")
    @ExcelProperty(value = "状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.MES_PRO_BAG_CODE_STATUS)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/MesProBatchController.java
@@ -213,21 +213,13 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 1. èŽ·å¾—å…³è”æ•°æ®ï¼ˆè´£ä»»äººã€å“ç§ç‰©æ–™ï¼‰
        // 1. èŽ·å¾—å…³è”æ•°æ®ï¼ˆè´£ä»»äººï¼‰ï¼›å“ç§å·²ä¸‹æ²‰åˆ°è¢‹ç æ•°æ®åŒ…ï¼Œæ‰¹æ¬¡åˆ—è¡¨ä¸å†å±•ç¤º
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(list, MesProBatchDO::getOwnerUserId));
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(list, MesProBatchDO::getItemId));
        // 2. æž„建结果
        return BeanUtils.toBean(list, MesProBatchRespVO.class, vo -> {
            MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user ->
                    vo.setOwnerUserName(user.getNickname()));
            MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode());
                vo.setItemName(item.getName());
                vo.setItemSpecification(item.getSpecification());
            });
        });
        return BeanUtils.toBean(list, MesProBatchRespVO.class, vo ->
                MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user ->
                        vo.setOwnerUserName(user.getNickname())));
    }
    private List<MesProBatchWorkerRespVO> buildWorkerRespVOList(List<MesProBatchWorkerDO> workers) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchPageReqVO.java
@@ -23,18 +23,18 @@
    @Schema(description = "临时批次码", example = "PB-00000001")
    private String tempCode;
    @Schema(description = "品种(产品物料)编号", example = "1")
    private Long itemId;
    @Schema(description = "产线编号", example = "1")
    private Long lineId;
    @Schema(description = "产线编码", example = "L01")
    private String lineCode;
    @Schema(description = "产线名称", example = "包装一线")
    @Schema(description = "产线名称(页面展示为「加工单位」)", example = "包装一线")
    private String lineName;
    @Schema(description = "加工班组", example = "一号加工班组")
    private String processingTeam;
    @Schema(description = "责任人用户编号", example = "1")
    private Long ownerUserId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchRespVO.java
@@ -30,41 +30,29 @@
    @ExcelIgnore
    private String tempCode;
    @Schema(description = "品种(产品物料)编号", example = "1")
    @ExcelIgnore
    private Long itemId;
    @Schema(description = "品种编码")
    @ExcelProperty("品种编码")
    private String itemCode;
    @Schema(description = "品种名称")
    @ExcelProperty("品种名称")
    private String itemName;
    @Schema(description = "品种规格")
    @ExcelProperty("品种规格")
    private String itemSpecification;
    @Schema(description = "产线编号", example = "1")
    @ExcelIgnore
    private Long lineId;
    @Schema(description = "产线编码", example = "L01")
    @ExcelProperty("产线编码")
    @Schema(description = "产线编码(页面已隐藏,仅接口返回)", example = "L01")
    @ExcelIgnore
    private String lineCode;
    @Schema(description = "产线名称", example = "包装一线")
    @ExcelProperty("产线名称")
    @Schema(description = "产线名称(页面展示为「加工单位」)", example = "包装一线")
    @ExcelProperty("加工单位")
    private String lineName;
    @Schema(description = "加工班组", example = "一号加工班组")
    @ExcelProperty("加工班组")
    private String processingTeam;
    @Schema(description = "生产日期")
    @ExcelProperty("生产日期")
    @ColumnWidth(20)
    private LocalDateTime produceDate;
    @Schema(description = "计划数量", example = "1000")
    @ExcelProperty("计划数量")
    @Schema(description = "加工数量(原计划数量,页面展示为「加工数量」)", example = "1000")
    @ExcelProperty("加工数量")
    private BigDecimal planQuantity;
    @Schema(description = "责任人用户编号", example = "1")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/batch/vo/MesProBatchSaveReqVO.java
@@ -4,6 +4,7 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -20,20 +21,20 @@
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "品种(产品物料)编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "品种不能为空")
    private Long itemId;
    @Schema(description = "产线编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "产线不能为空")
    @Schema(description = "产线编号(页面展示为「加工单位」)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "加工单位不能为空")
    private Long lineId;
    @Schema(description = "加工班组(手工填写)", example = "一号加工班组")
    @Size(max = 64, message = "加工班组长度不能超过 64")
    private String processingTeam;
    @Schema(description = "生产日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "生产日期不能为空")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime produceDate;
    @Schema(description = "计划数量", example = "1000")
    @Schema(description = "加工数量(原计划数量,页面展示为「加工数量」)", example = "1000")
    private BigDecimal planQuantity;
    @Schema(description = "责任人用户编号(责任落实到人)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@@ -48,6 +49,9 @@
    @Valid
    private List<Worker> workers;
    @Schema(description = "绑定托盘编号列表(可选;绑定时批次直接进入生产中,托盘下袋码级联录入本批次;新增/补录/编辑均可传入,编辑时仅需传本次新增勾选的托盘)")
    private List<Long> palletIds;
    @Schema(description = "生产人员配置")
    @Data
    public static class Worker {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/MesProPalletController.java
@@ -159,6 +159,14 @@
        return success(BeanUtils.toBean(list, MesProPalletRespVO.class));
    }
    @GetMapping("/unbound-list")
    @Operation(summary = "获得未绑定批次的临时托盘列表(供批次新增/补录绑定托盘下拉使用)")
    @PreAuthorize("@ss.hasPermission('mes:pro-pallet:query')")
    public CommonResult<List<MesProPalletRespVO>> getUnboundPalletList() {
        List<MesProPalletDO> list = palletService.getUnboundPalletList();
        return success(BeanUtils.toBean(list, MesProPalletRespVO.class));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出托盘码 Excel")
    @PreAuthorize("@ss.hasPermission('mes:pro-pallet:export')")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletBatchCreateReqVO.java
@@ -18,10 +18,6 @@
    @Positive(message = "生成数量必须大于 0")
    private Integer count;
    @Schema(description = "可装袋数(袋/托盘)", example = "25")
    @Positive(message = "可装袋数必须大于 0")
    private Integer capacity;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletPreGenerateReqVO.java
@@ -14,10 +14,6 @@
    @Positive(message = "生成数量必须大于 0")
    private Integer count;
    @Schema(description = "可装袋数(袋/托盘)", example = "25")
    @Positive(message = "可装袋数必须大于 0")
    private Integer capacity;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/pallet/vo/MesProPalletRespVO.java
@@ -41,10 +41,6 @@
    @ExcelProperty("托盘序号")
    private Integer palletNo;
    @Schema(description = "可装袋数(袋/托盘)", example = "25")
    @ExcelProperty("可装袋数")
    private Integer capacity;
    @Schema(description = "已绑定袋码数量", example = "10")
    @ExcelProperty("已装袋数")
    private Integer bagCount;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/MesProScanEventController.java
@@ -23,7 +23,7 @@
@RestController @RequestMapping("/mes/pro/scan-in")
public class MesProScanEventController {
    @Resource private MesProScanEventService service;
    @PostMapping("/inbound") @PreAuthorize("@ss.hasPermission('mes:pro-scan-in:inbound')") @Operation(summary = "袋码扫码入库")
    @PostMapping("/inbound") @PreAuthorize("@ss.hasPermission('mes:pro-scan-in:inbound')") @Operation(summary = "追溯码扫码入库(袋码/托盘码/批次码)")
    public CommonResult<MesProScanEventRespVO> inbound(@Valid @RequestBody MesProScanEventCreateReqVO req) {
        return success(BeanUtils.toBean(service.createInbound(req), MesProScanEventRespVO.class));
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/vo/MesProScanEventCreateReqVO.java
@@ -8,9 +8,19 @@
@Data
public class MesProScanEventCreateReqVO {
    @NotBlank private String eventId;
    /**
     * æ‰«ç å†…容(袋码 / æ‰˜ç›˜ç  / æ‰¹æ¬¡ç çš„码值,含临时码)
     */
    @NotBlank private String bagCode;
    /**
     * æ‰«ç ç±»åž‹ï¼šBAG è¢‹ç  / PALLET æ‰˜ç›˜ç  / BATCH æ‰¹æ¬¡ç ï¼Œé»˜è®¤ BAG
     */
    private String scanType;
    @NotNull @DecimalMin("0.000001") private BigDecimal quantity;
    @NotBlank private String deviceCode;
    /**
     * è®¾å¤‡ç¼–码(不再必填,产线扫码以产线+库区库位定位)
     */
    @Size(max = 64) private String deviceCode;
    private String lineCode;
    @NotNull private Long locationId;
    @NotNull private Long areaId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/vo/MesProScanEventRespVO.java
@@ -10,6 +10,7 @@
    private Long id;
    private String eventId;
    private String bagCode;
    private String scanType;
    private String batchCode;
    private BigDecimal quantity;
    private String deviceCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/trace/MesProTraceResolveController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.trace;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MES æ‰«ç è¿½æº¯ - ç¼–码类型解析。
 *
 * <p>编码统一为「16 ä½çº¯æ•°å­—」后,无法再按前缀判断类型(BAG-/PL-/PB- ç­‰å‰ç¼€å·²å–消),
 * å‰ç«¯æ‰«ç åŽå…ˆè°ƒç”¨æœ¬æŽ¥å£æŒ‰ç¼–码反查其所属类型,再调用对应类型的追溯接口。
 */
@Tag(name = "管理后台 - MES æ‰«ç æº¯æºï¼ˆç±»åž‹è§£æžï¼‰")
@RestController
@RequestMapping("/mes/pro/trace")
@Validated
public class MesProTraceResolveController {
    @Resource
    private MesProBagCodeService bagCodeService;
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesProBatchService batchService;
    @GetMapping("/resolve-type")
    @Operation(summary = "根据编码解析溯源类型:bag / pallet / batch;均不匹配返回空")
    @Parameter(name = "code", description = "编码(二维码内容)", required = true)
    @PreAuthorize("@ss.hasPermission('mes:pro-bag-code:query')")
    public CommonResult<String> resolveType(@RequestParam("code") String code) {
        if (bagCodeService.getBagCodeByCode(code) != null) {
            return success("bag");
        }
        if (palletService.getPalletByCode(code) != null) {
            return success("pallet");
        }
        if (batchService.getBatchByCode(code) != null) {
            return success("batch");
        }
        return success(null);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
@@ -1,6 +1,10 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedQualityService;
import io.swagger.v3.oas.annotations.Operation;
@@ -16,6 +20,13 @@
@RestController @RequestMapping("/mes/qc/seed-quality") @Validated
public class MesQcSeedQualityController {
    @Resource private MesQcSeedQualityService service;
    @GetMapping("/page")
    @Operation(summary = "获得种子质检记录分页")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:query')")
    public CommonResult<PageResult<MesQcSeedQualityRespVO>> getQualityPage(@Valid MesQcSeedQualityPageReqVO pageReqVO) {
        return success(BeanUtils.toBean(service.getQualityPage(pageReqVO), MesQcSeedQualityRespVO.class));
    }
    @PostMapping("/submit") @Operation(summary = "提交种子质检")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:submit')")
    public CommonResult<Long> submit(@Valid @RequestBody MesQcSeedQualitySubmitReqVO reqVO) { return success(service.submit(reqVO)); }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - MES ç§å­è´¨æ£€è®°å½•分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class MesQcSeedQualityPageReqVO extends PageParam {
    @Schema(description = "质检袋码")
    private String code;
    @Schema(description = "生产批次")
    private String batchCode;
    @Schema(description = "检验结果:1合格 2不合格")
    private Integer result;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES ç§å­è´¨æ£€è®°å½• Response VO")
@Data
public class MesQcSeedQualityRespVO {
    private Long id;
    private String code;
    private Long itemId;
    private Long batchId;
    private String batchCode;
    private Integer result;
    private String reason;
    private Long inspectorId;
    private LocalDateTime inspectedAt;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualitySubmitReqVO.java
@@ -9,10 +9,11 @@
@Data
public class MesQcSeedQualitySubmitReqVO {
    @NotBlank private String code;
    @NotNull private Long itemId;
    private String batchCode;
    @NotEmpty @Valid private List<Indicator> indicators;
    private String reason;
    /** ç›®æ ‡åˆæ ¼åº“编号(质检合格后入该仓库),必须是仓库类型为合格库(20)的仓库 */
    @NotNull private Long qualifiedWarehouseId;
    @Data public static class Indicator {
        @NotBlank private String code;
        @NotNull @DecimalMin("0") private BigDecimal value;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/trace/batch/MesTraceBatchController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.controller.admin.trace.batch;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicRespVO;
import cn.iocoder.yudao.module.mes.service.trace.MesProTracePublicService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotBlank;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES æ‰¹æ¬¡è¿½æº¯")
@RestController
@RequestMapping("/mes/trace/batch")
@Validated
public class MesTraceBatchController {
    @Resource
    private MesProTracePublicService traceService;
    @GetMapping("/detail")
    @Operation(summary = "查询统一批次追溯详情")
    @Parameter(name = "code", description = "批次码、临时批次码、袋码或托盘码", required = true,
            example = "BAG-202608280001")
    @PreAuthorize("@ss.hasPermission('mes:trace-batch:query')")
    public CommonResult<MesProTracePublicRespVO> getBatchTraceDetail(
            @RequestParam("code") @NotBlank(message = "追溯码不能为空") String code) {
        return success(traceService.queryTrace(code));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchFlowRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ‰¹æ¬¡å‡ºå…¥åº“流水(种子流向)Response VO")
@Data
public class MesWmBatchFlowRespVO {
    @Schema(description = "事务类型:1-入库 2-出库", example = "2")
    private Integer type;
    @Schema(description = "业务类型(见 MesBizTypeConstants,如 119-销售出库、124-产品产出)", example = "119")
    private Integer bizType;
    @Schema(description = "来源业务单号", example = "SL_20260901002")
    private String bizCode;
    @Schema(description = "本次变动数量(出库为负数)", example = "-1")
    private BigDecimal quantity;
    @Schema(description = "事务发生时间")
    private LocalDateTime transactionTime;
    @Schema(description = "仓库名称", example = "成品合格库")
    private String warehouseName;
    @Schema(description = "库区名称", example = "成品合格库区")
    private String locationName;
    @Schema(description = "库位名称", example = "成品合格库位")
    private String areaName;
    @Schema(description = "客户名称(销售出库/销售退货时)", example = "客户A")
    private String clientName;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java
@@ -14,6 +14,9 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockBagPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockBagRespVO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmStockSummaryRespVO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
@@ -21,6 +24,7 @@
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
@@ -72,6 +76,8 @@
    private MesWmWarehouseAreaService areaService;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private MesWmMaterialStockBagMapper materialStockBagMapper;
    @GetMapping("/get")
    @Operation(summary = "获得库存记录")
@@ -92,6 +98,17 @@
            @Valid MesWmMaterialStockPageReqVO pageReqVO) {
        PageResult<MesWmMaterialStockDO> pageResult = materialStockService.getMaterialStockPage(pageReqVO);
        return success(new PageResult<>(buildRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/bag-details")
    @Operation(summary = "查询库存袋码明细")
    @PreAuthorize("@ss.hasPermission('mes:wm-material-stock:query')")
    public CommonResult<PageResult<MesWmMaterialStockBagRespVO>> getMaterialStockBagDetails(
            @Valid MesWmMaterialStockBagPageReqVO pageReqVO) {
        PageResult<MesWmMaterialStockBagRespVO> result = new PageResult<>();
        PageResult<cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO> page =
                materialStockBagMapper.selectPage(pageReqVO);
        return success(new PageResult<>(BeanUtils.toBean(page.getList(), MesWmMaterialStockBagRespVO.class), page.getTotal()));
    }
    @PutMapping("/update-frozen")
@@ -179,6 +196,9 @@
                convertSet(list, MesWmMaterialStockDO::getLocationId));
        Map<Long, MesWmWarehouseAreaDO> areaMap = areaService.getWarehouseAreaMap(
                convertSet(list, MesWmMaterialStockDO::getAreaId));
        Map<Long, Long> bagCountMap = materialStockBagMapper.selectListByMaterialStockIds(
                convertSet(list, MesWmMaterialStockDO::getId)).stream()
                .collect(Collectors.groupingBy(MesWmMaterialStockBagDO::getMaterialStockId, Collectors.counting()));
        Map<Long, SrmSupplierRespDTO> vendorMap = srmSupplierApi.getSupplierList(
                convertSet(list, MesWmMaterialStockDO::getVendorId)).getCheckedData().stream()
                .collect(Collectors.toMap(SrmSupplierRespDTO::getId, s -> s));
@@ -197,6 +217,8 @@
                    area -> vo.setAreaName(area.getName()));
            MapUtils.findAndThen(vendorMap, vo.getVendorId(),
                    vendor -> vo.setVendorName(vendor.getName()));
            vo.setBagCount(bagCountMap.getOrDefault(vo.getId(), 0L));
            vo.setBagDetailSupported(bagCountMap.containsKey(vo.getId()));
            // è®¡ç®—可用量 = åœ¨åº“数量 - å†»ç»“数量 - å ç”¨é‡
            BigDecimal quantity = vo.getQuantity() != null ? vo.getQuantity() : BigDecimal.ZERO;
            BigDecimal frozenQty = vo.getFrozenQuantity() != null ? vo.getFrozenQuantity() : BigDecimal.ZERO;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockBagPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - åº“存袋码明细分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class MesWmMaterialStockBagPageReqVO extends PageParam {
    @Schema(description = "库存汇总记录ID", requiredMode = Schema.RequiredMode.REQUIRED)
    private Long materialStockId;
    @Schema(description = "袋码")
    private String bagCode;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockBagRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - åº“存袋码明细 Response VO")
@Data
public class MesWmMaterialStockBagRespVO {
    private Long id;
    private Long materialStockId;
    private Long bagCodeId;
    private String bagCode;
    private Long itemId;
    private Long batchId;
    private String batchCode;
    private Long warehouseId;
    private Long locationId;
    private Long areaId;
    private Integer stockState;
    private Integer status;
    private BigDecimal quantity;
    private Boolean frozen;
    private LocalDateTime inboundTime;
    private LocalDateTime outboundTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockListReqVO.java
@@ -12,6 +12,9 @@
    @Schema(description = "仓库编号")
    private Long warehouseId;
    @Schema(description = "仓库类型:10暂存库,20合格库(按类型过滤仓库,与 warehouseId äºŒé€‰ä¸€ï¼‰", example = "20")
    private Integer warehouseType;
    @Schema(description = "库区编号")
    private Long locationId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockPageReqVO.java
@@ -39,6 +39,9 @@
    @Schema(description = "仓库编号", example = "1")
    private Long warehouseId;
    @Schema(description = "仓库类型:10暂存库,20合格库(按类型过滤仓库,与 warehouseId äºŒé€‰ä¸€ï¼‰", example = "20")
    private Integer warehouseType;
    @Schema(description = "库区编号", example = "1")
    private Long locationId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockRespVO.java
@@ -46,6 +46,12 @@
    @ExcelProperty("批次号")
    private String batchCode;
    @Schema(description = "当前袋码数量", example = "10")
    private Long bagCount;
    @Schema(description = "是否存在袋码明细", example = "true")
    private Boolean bagDetailSupported;
    @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long warehouseId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesRespVO.java
@@ -42,10 +42,6 @@
    @ExcelProperty("出库操作人")
    private String outboundUserName;
    @Schema(description = "销售订单号", example = "SO2026030001")
    @ExcelProperty("销售订单号")
    private String salesOrderCode;
    @Schema(description = "出库日期")
    @ExcelProperty("出库日期")
    private LocalDateTime salesDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesSaveReqVO.java
@@ -26,9 +26,6 @@
    @NotNull(message = "客户不能为空")
    private Long clientId;
    @Schema(description = "销售订单号", example = "SO2026030001")
    private String salesOrderCode;
    @Schema(description = "出库日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "出库日期不能为空")
    private LocalDateTime salesDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesPageReqVO.java
@@ -26,12 +26,6 @@
    @Schema(description = "退货单名称", example = "销售退货")
    private String name;
    @Schema(description = "销售订单 ID", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单编号", example = "SO202603010001")
    private String salesOrderCode;
    @Schema(description = "客户 ID", example = "1")
    private Long clientId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java
@@ -24,12 +24,6 @@
    @Schema(description = "退货单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "销售退货")
    private String name;
    @Schema(description = "销售订单 ID", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单编号", example = "SO202603010001")
    private String salesOrderCode;
    @Schema(description = "客户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long clientId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java
@@ -27,12 +27,6 @@
    @NotBlank(message = "退货单名称不能为空")
    private String name;
    @Schema(description = "销售订单 ID", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单编号", example = "SO202603010001")
    private String salesOrderCode;
    @Schema(description = "客户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "客户 ID ä¸èƒ½ä¸ºç©º")
    private Long clientId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/MesWmSalesNoticeController.java
@@ -111,14 +111,6 @@
        return success(true);
    }
    @PostMapping("/generate-from-sale-order")
    @Operation(summary = "从销售订单生成发货通知单")
    @PreAuthorize("@ss.hasPermission('mes:wm-sales-notice:create')")
    public CommonResult<Long> generateFromSaleOrder(@RequestBody Map<String, Long> params) {
        Long saleOrderId = params.get("saleOrderId");
        return success(salesNoticeService.generateSalesNoticeFromSaleOrder(saleOrderId));
    }
    @PostMapping("/generate-product-sales")
    @Operation(summary = "从发货通知单生成销售出库单")
    @Parameter(name = "id", description = "发货通知单编号", required = true)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/MesWmSalesNoticePageReqVO.java
@@ -18,9 +18,6 @@
    @Schema(description = "通知单名称", example = "测试发货通知")
    private String name;
    @Schema(description = "销售订单编号", example = "SO202603010001")
    private String saleOrderCode;
    @Schema(description = "客户编号", example = "1")
    private Long clientId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/MesWmSalesNoticeRespVO.java
@@ -31,13 +31,6 @@
    @ExcelProperty("客户名称")
    private String clientName;
    @Schema(description = "销售订单ID", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单号", example = "SO202603010001")
    @ExcelProperty("销售订单号")
    private String saleOrderCode;
    @Schema(description = "发货日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("发货日期")
    private LocalDateTime salesDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/MesWmSalesNoticeSaveReqVO.java
@@ -26,12 +26,6 @@
    @NotNull(message = "客户编号不能为空")
    private Long clientId;
    @Schema(description = "销售订单ID", example = "1")
    private Long saleOrderId;
    @Schema(description = "销售订单号", example = "SO202603010001")
    private String saleOrderCode;
    @Schema(description = "发货日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "发货日期不能为空")
    private LocalDateTime salesDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/line/MesWmSalesNoticeLineRespVO.java
@@ -16,9 +16,6 @@
    @Schema(description = "发货通知单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long noticeId;
    @Schema(description = "销售订单明细ID", example = "1")
    private Long saleOrderItemId;
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long itemId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/line/MesWmSalesNoticeLineSaveReqVO.java
@@ -18,9 +18,6 @@
    @NotNull(message = "发货通知单编号不能为空")
    private Long noticeId;
    @Schema(description = "销售订单明细ID", example = "1")
    private Long saleOrderItemId;
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "物料编号不能为空")
    private Long itemId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java
@@ -119,6 +119,12 @@
            return "未知";
        }
        switch (bizType) {
            case MesBizTypeConstants.QC_SEED_QUALITY:
                return "种子质检";
            case MesBizTypeConstants.WM_ITEM_RECEIPT_CANCEL:
                return "采购入库取消";
            case MesBizTypeConstants.WM_STOCK_TAKING:
                return "盘点调整";
            case MesBizTypeConstants.WM_STOCK_ADJUST_IN:
                return "自定义入库";
            case MesBizTypeConstants.WM_STOCK_ADJUST_OUT:
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionPageReqVO.java
@@ -26,6 +26,9 @@
    @Schema(description = "批次号", example = "BATCH001")
    private String batchCode;
    @Schema(description = "袋码", example = "BG202608180001")
    private String bagCode;
    @Schema(description = "仓库ID", example = "1")
    private Long warehouseId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionRespVO.java
@@ -51,6 +51,12 @@
    @Schema(description = "批次号", example = "BATCH001")
    private String batchCode;
    @Schema(description = "袋码ID", example = "1")
    private Long bagCodeId;
    @Schema(description = "袋码", example = "BG202608180001")
    private String bagCode;
    // ========== åº“存位置 ==========
    @Schema(description = "仓库ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehousePageReqVO.java
@@ -21,4 +21,7 @@
    @Schema(description = "是否冻结", example = "false")
    private Boolean frozen;
    @Schema(description = "仓库类型:10暂存库 20合格库", example = "20")
    private Integer type;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehouseRespVO.java
@@ -40,6 +40,10 @@
    @ExcelProperty("是否冻结")
    private Boolean frozen;
    @Schema(description = "仓库类型:10暂存库 20合格库", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
    @ExcelProperty("仓库类型")
    private Integer type;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehouseSaveReqVO.java
@@ -35,6 +35,10 @@
    @NotNull(message = "是否冻结不能为空")
    private Boolean frozen;
    @Schema(description = "仓库类型:10暂存库 20合格库", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
    @NotNull(message = "仓库类型不能为空")
    private Integer type;
    @Schema(description = "备注", example = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/MesProTracePublicController.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.controller.pub.trace;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicConfigRespVO;
import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicRespVO;
import cn.iocoder.yudao.module.mes.service.trace.MesProTracePublicService;
import io.swagger.v3.oas.annotations.Operation;
@@ -38,4 +39,11 @@
        return success(service.queryTrace(code));
    }
    @GetMapping("/config")
    @PermitAll
    @Operation(summary = "获得溯源页配置(生产经营者名称、追溯网址)")
    public CommonResult<MesProTracePublicConfigRespVO> getConfig() {
        return success(service.getPublicConfig());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/vo/MesProTracePublicConfigRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.controller.pub.trace.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * H5 æ‰«ç æº¯æº - é¡µé¢é…ç½® Response VO
 * <p>
 * å‘ H5 æº¯æºé¡µä¸‹å‘「生产经营者名称」与「追溯网址」,二者均为种子标签四要素的组成部分。
 * ç”±åŽç«¯ç»Ÿä¸€ä¸‹å‘,保证二维码内容、标签印刷内容与 H5 é¡µé¢å±•示三者一致,
 * é¿å…å‰ç«¯ç¡¬ç¼–码导致的多真值源不一致问题。
 */
@Schema(description = "H5 æ‰«ç æº¯æº - é¡µé¢é…ç½® Response VO")
@Data
public class MesProTracePublicConfigRespVO {
    @Schema(description = "生产经营者名称(进口种子场景为进口商名称)", example = "新疆塔里木河种业股份有限公司")
    private String operatorName;
    @Schema(description = "追溯网址(与二维码内容前缀一致,不含 code å‚数)",
            example = "http://1.95.78.124:9008/prod/trace.html")
    private String traceUrl;
    @Schema(description = "是否进口种子场景:为 true æ—¶é¡µé¢æ ‡ç­¾åº”展示「进口商名称」")
    private Boolean imported;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/vo/MesProTracePublicRespVO.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.mes.controller.pub.trace.vo;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
@@ -14,7 +15,7 @@
 * H5 æ‰«ç æº¯æº - å…¨ç¨‹æº¯æºä¿¡æ¯ Response VO
 * <p>
 * ä»¥ã€Œä¿¡æ¯åŒºå—」模块化组织,核心信息(品种/批次/人员/质检/入库)始终返回;
 * é‡‡è´­æ¥æºã€é”€å”®åŽ»å‘ã€åŠ å·¥æµè½¬ä¸ºå¯é€‰æ‰©å±•åŒºå—ï¼ˆæœ‰æ•°æ®æ‰éžç©ºï¼‰ã€‚åŽç»­æ–°å¢žæ•°æ®æºæŒ‰åŒä¸€æ¨¡å¼è¿½åŠ åŒºå—å­—æ®µã€‚
 * é‡‡è´­æ¥æºã€é”€å”®åŽ»å‘ã€ç§å­æµå‘ï¼ˆæ‰¹æ¬¡å‡ºå…¥åº“æµæ°´ï¼‰ã€åŠ å·¥æµè½¬ä¸ºå¯é€‰æ‰©å±•åŒºå—ï¼ˆæœ‰æ•°æ®æ‰éžç©ºï¼‰ã€‚åŽç»­æ–°å¢žæ•°æ®æºæŒ‰åŒä¸€æ¨¡å¼è¿½åŠ åŒºå—å­—æ®µã€‚
 */
@Schema(description = "H5 æ‰«ç æº¯æº - å…¨ç¨‹æº¯æºä¿¡æ¯ Response VO")
@Data
@@ -23,8 +24,24 @@
    @Schema(description = "追溯入口类型:BAG-袋码 / PALLET-托盘码 / BATCH-批次码", example = "BAG")
    private String sourceType;
    @Schema(description = "请求的追溯码(原始扫码值)", example = "PC20260828twoITEM_001")
    @Schema(description = "请求的追溯码(原始扫码值)", example = "0286267849370923")
    private String requestCode;
    @Schema(description = "追溯码有效性:VALID-有效 / CANCELED-已作废(作废码不得作为有效产品流通)",
            example = "VALID")
    private String codeStatus;
    @Schema(description = "是否已录入生产批次(false è¡¨ç¤ºè¢‹ç /托盘已生成但尚未录入批次,此时仅返回品种信息)", example = "true")
    private Boolean batchBound;
    // ====== æ ‡ç­¾å››è¦ç´ åŒºå—(与二维码标签印刷内容保持一致) ======
    @Schema(description = "生产经营者名称(进口种子场景为进口商名称)",
            example = "新疆塔里木河种业股份有限公司")
    private String operatorName;
    @Schema(description = "追溯网址(与二维码内容前缀一致)",
            example = "http://1.95.78.124:9008/prod/trace.html")
    private String traceUrl;
    // ====== å“ç§ä¿¡æ¯åŒºå— ======
    @Schema(description = "品种编码")
@@ -35,6 +52,9 @@
    @Schema(description = "品种规格")
    private String itemSpecification;
    @Schema(description = "品种计量单位名称", example = "48千克")
    private String itemUnitMeasureName;
    // ====== ç”Ÿäº§æ‰¹æ¬¡ä¿¡æ¯åŒºå— ======
    @Schema(description = "批次号")
@@ -116,9 +136,50 @@
    @Schema(description = "销售去向(可选)")
    private List<MesWmBatchSalesTargetRespVO> salesTargets;
    @Schema(description = "种子流向信息(批次出入库流水:生产入库 â†’ é”€å”®å‡ºåº“等,可选)")
    private List<MesWmBatchFlowRespVO> seedFlows;
    @Schema(description = "加工流转(可选)")
    private List<MesWmBatchProcessTraceRespVO> processTraces;
    @Schema(description = "追溯时间线")
    private List<TimelineVO> timeline;
    @Schema(description = "追溯决策摘要")
    private DecisionVO decision;
    @Schema(description = "追溯时间线事件")
    @Data
    public static class TimelineVO {
        @Schema(description = "阶段")
        private String stage;
        @Schema(description = "事件类型")
        private String eventType;
        @Schema(description = "事件名称")
        private String eventName;
        @Schema(description = "事件时间")
        private LocalDateTime eventTime;
        @Schema(description = "数量")
        private BigDecimal quantity;
        @Schema(description = "状态")
        private String status;
    }
    @Schema(description = "追溯决策摘要")
    @Data
    public static class DecisionVO {
        @Schema(description = "质量是否存在风险")
        private Boolean qualityRisk;
        @Schema(description = "库存入库数量")
        private BigDecimal inboundQuantity;
        @Schema(description = "销售去向数量")
        private Integer salesTargetCount;
        @Schema(description = "消费者扫码次数")
        private Long consumerScanCount;
        @Schema(description = "风险等级")
        private String riskLevel;
    }
    @Schema(description = "生产人员")
    @Data
    public static class WorkerVO {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/bagcode/MesProBagCodeDO.java
@@ -28,7 +28,20 @@
    private String batchCode;
    private Long palletId;
    private String palletCode;
    /**
     * å“ç§ï¼ˆç‰©æ–™ï¼‰ç¼–号(生成数据包时选定,关联 mdm_item)
     */
    private Long itemId;
    /**
     * å“ç§ç¼–码(冗余)
     */
    private String itemCode;
    /**
     * å“ç§åç§°ï¼ˆå†—余)
     */
    private String itemName;
    private Integer status;
    private Integer qcStatus;
    private LocalDateTime printTime;
    private LocalDateTime cancelTime;
    private String cancelReason;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/batch/MesProBatchDO.java
@@ -14,8 +14,9 @@
/**
 * MES ç”Ÿäº§æ‰¹æ¬¡ DO
 * <p>
 * æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†ï¼ˆä¸€è¢‹ä¸€ç æ¨¡å—):关联品种(MDM ç‰©æ–™ï¼‰ã€äº§çº¿ï¼ˆäº§çº¿ç»´æŠ¤ï¼‰ä¸Žç”Ÿäº§äººå‘˜ï¼ˆäººå‘˜é…ç½®è§ {@link MesProBatchWorkerDO}),责任落实到人。
 * æ‰¹æ¬¡å·è‡ªåŠ¨ç”Ÿæˆï¼Œç¼–ç è§„åˆ™åŒ…å«æ—¥æœŸã€äº§çº¿ç¼–ç ã€å“ç§ç¼–ç ã€æµæ°´å·ç­‰è¦ç´ ï¼Œå¹¶ä¸Žè¢‹ç ï¼ˆä¸€è¢‹ä¸€ç ï¼‰ã€æ‰˜ç›˜ç è‡ªåŠ¨å…³è”ã€‚
 * æ‰¹æ¬¡ç”Ÿäº§ç®¡ç†ï¼ˆä¸€è¢‹ä¸€ç æ¨¡å—):关联产线(产线维护)与生产人员(人员配置见 {@link MesProBatchWorkerDO}),责任落实到人。
 * å“ç§å·²ä¸‹æ²‰åˆ°è¢‹ç æ•°æ®åŒ…(赋码管理),批次不再绑定品种。
 * æ‰¹æ¬¡å·è‡ªåŠ¨ç”Ÿæˆï¼Œç¼–ç è§„åˆ™åŒ…å«æ—¥æœŸã€äº§çº¿ç¼–ç ã€æµæ°´å·ç­‰è¦ç´ ï¼Œå¹¶ä¸Žè¢‹ç ï¼ˆä¸€è¢‹ä¸€ç ï¼‰ã€æ‰˜ç›˜ç è‡ªåŠ¨å…³è”ã€‚
 *
 * @author è¶…级管理员
 */
@@ -35,7 +36,7 @@
    @TableId
    private Long id;
    /**
     * æ‰¹æ¬¡å·ï¼ˆè‡ªåŠ¨ç”Ÿæˆï¼šå‰ç¼€ + æ—¥æœŸ + äº§çº¿ç¼–码 + å“ç§ç¼–码 + æµæ°´å·ï¼›è¡¥å½•业务信息后生成)
     * æ‰¹æ¬¡å·ï¼ˆè‡ªåŠ¨ç”Ÿæˆï¼šå‰ç¼€ + æ—¥æœŸ + äº§çº¿ç¼–码 + æµæ°´å·ï¼›è¡¥å½•业务信息后生成)
     */
    private String code;
    /**
@@ -43,9 +44,11 @@
     */
    private String tempCode;
    /**
     * å“ç§ï¼ˆäº§å“ç‰©æ–™ï¼‰ç¼–号
     * å“ç§ï¼ˆäº§å“ç‰©æ–™ï¼‰ç¼–号(已废弃)
     * <p>
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO#getId()}
     * <p>
     * å“ç§å·²ä¸‹æ²‰åˆ°è¢‹ç æ•°æ®åŒ…({@code mes_pro_bag_code.item_id}),新批次不再写入;仅保留供历史批次追溯回退
     */
    private Long itemId;
    /**
@@ -59,10 +62,14 @@
     */
    private String lineCode;
    /**
     * äº§çº¿åç§°ï¼ˆå†—余列,由 {@link #lineId} å›žå¡«ï¼‰
     * äº§çº¿åç§°ï¼ˆå†—余列,由 {@link #lineId} å›žå¡«ï¼Œé¡µé¢å±•示为「加工单位」)
     */
    private String lineName;
    /**
     * åŠ å·¥ç­ç»„ï¼ˆæ‰‹å·¥å¡«å†™ï¼‰
     */
    private String processingTeam;
    /**
     * ç”Ÿäº§æ—¥æœŸ
     */
    private LocalDateTime produceDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/pallet/MesProPalletDO.java
@@ -62,6 +62,14 @@
     */
    private Integer bagCount;
    /**
     * æœ€è¿‘入库扫码事件编号(整托扫码入库后回写,用于防重复入库)
     */
    private Long inboundEventId;
    /**
     * æœ€è¿‘入库时间
     */
    private java.time.LocalDateTime inboundTime;
    /**
     * æ˜¯å¦è‡ªåŠ¨ç”Ÿæˆï¼ˆæŒ‰æœ‰æ•ˆè¢‹ç ç´¯è®¡é˜ˆå€¼ç”Ÿæˆï¼Œå®¹é‡å›ºå®šä¸ºé˜ˆå€¼ï¼‰
     */
    private Boolean autoGenerated;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/scan/MesProScanEventDO.java
@@ -14,7 +14,14 @@
public class MesProScanEventDO extends BaseDO {
    @TableId private Long id;
    private String eventId;
    /**
     * æ‰«ç å†…容(袋码 / æ‰˜ç›˜ç  / æ‰¹æ¬¡ç çš„码值,含临时码)
     */
    private String bagCode;
    /**
     * æ‰«ç ç±»åž‹ï¼šBAG è¢‹ç  / PALLET æ‰˜ç›˜ç  / BATCH æ‰¹æ¬¡ç 
     */
    private String scanType;
    private Long bagCodeId;
    private Long batchId;
    private String batchCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java
@@ -12,6 +12,8 @@
    @TableId private Long id;
    private String code;
    private Long itemId;
    /** ç”Ÿäº§æ‰¹æ¬¡ ID */
    private Long batchId;
    private String batchCode;
    private Integer result;
    private String reason;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/batch/MesWmBatchDO.java
@@ -3,6 +3,7 @@
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.tm.tool.MesTmToolDO;
@@ -34,6 +35,12 @@
    @TableId
    private Long id;
    /**
     * å¯¹åº”生产批次 ID
     *
     * å…³è” {@link MesProBatchDO#getId()}
     */
    private Long proBatchId;
    /**
     * æ‰¹æ¬¡ç¼–码
     */
    private String code;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/materialstockbag/MesWmMaterialStockBagDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * MES åº“存袋码明细 DO
 */
@TableName("mes_wm_material_stock_bag")
@KeySequence("mes_wm_material_stock_bag_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesWmMaterialStockBagDO extends BaseDO {
    @TableId
    private Long id;
    private Long materialStockId;
    private Long bagCodeId;
    private String bagCode;
    private Long itemId;
    private Long batchId;
    private String batchCode;
    private Long vendorId;
    private Long warehouseId;
    private Long locationId;
    private Long areaId;
    private Integer stockState;
    private Integer status;
    private BigDecimal quantity;
    private Boolean frozen;
    private Long lastTransactionId;
    private LocalDateTime inboundTime;
    private LocalDateTime outboundTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productsales/MesWmProductSalesDO.java
@@ -50,14 +50,6 @@
     */
    private String clientName;
    /**
     * é”€å”®è®¢å•ID
     */
    private Long saleOrderId;
    /**
     * é”€å”®è®¢å•号
     */
    private String salesOrderCode;
    /**
     * å‘货通知单 ID
     *
     * å…³è” {@link MesWmSalesNoticeDO#getId()}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productsales/MesWmProductSalesLineDO.java
@@ -42,10 +42,6 @@
     */
    private Long salesId;
    /**
     * é”€å”®è®¢å•明细ID
     */
    private Long saleOrderItemId;
    /**
     * å‘货通知单行ID
     *
     * å…³è” {@link MesWmSalesNoticeLineDO#getId()}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java
@@ -39,14 +39,6 @@
     */
    private String name;
    /**
     * é”€å”®è®¢å• ID
     */
    private Long saleOrderId;
    /**
     * é”€å”®è®¢å•编号
     */
    private String salesOrderCode;
    /**
     * å®¢æˆ· ID
     *
     * å…³è” CRM å®¢æˆ· {@link cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO#getId()}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/salesnotice/MesWmSalesNoticeDO.java
@@ -42,14 +42,6 @@
     */
    private Long clientId;
    /**
     * é”€å”®è®¢å•ID
     */
    private Long saleOrderId;
    /**
     * é”€å”®è®¢å•号
     */
    private String saleOrderCode;
    /**
     * å‘货日期
     */
    private LocalDateTime salesDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/salesnotice/MesWmSalesNoticeLineDO.java
@@ -35,10 +35,6 @@
     */
    private Long noticeId;
    /**
     * é”€å”®è®¢å•明细ID
     */
    private Long saleOrderItemId;
    /**
     * ç‰©æ–™ç¼–号
     *
     * å…³è” {@link MesMdItemDO#getId()}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/sn/MesWmSnDO.java
@@ -2,6 +2,9 @@
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -29,6 +32,28 @@
    @TableId
    private Long id;
    /**
     * ç”Ÿäº§æ‰¹æ¬¡ ID
     *
     * å…³è” {@link MesProBatchDO#getId()}
     */
    private Long proBatchId;
    /**
     * è¢‹ç  ID
     *
     * å…³è” {@link MesProBagCodeDO#getId()}
     */
    private Long bagCodeId;
    /**
     * æ‰˜ç›˜ ID
     *
     * å…³è” {@link MesProPalletDO#getId()}
     */
    private Long palletId;
    /**
     * é”€å”®å‡ºåº“单 ID
     */
    private Long salesId;
    /**
     * æ‰¹æ¬¡ UUID(用于标记同一批次生成的 SN ç ï¼‰
     */
    private String uuid;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/transaction/MesWmTransactionDO.java
@@ -107,6 +107,10 @@
     * å…³è” {@link MesWmBatchDO#getCode()}
     */
    private String batchCode;
    /** è¢‹ç ID */
    private Long bagCodeId;
    /** è¢‹ç  */
    private String bagCode;
    // ==================== åº“存位置 ====================
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warehouse/MesWmWarehouseDO.java
@@ -62,6 +62,13 @@
     */
    private Boolean frozen;
    /**
     * ä»“库类型
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum}:
     * 10 æš‚存库(如虚拟线边仓库,存放扫码入库待检/暂存库存);20 åˆæ ¼åº“(存放质检合格、可拣货库存)
     */
    private Integer type;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/home/MesBagCodeHomeStatisticsMapper.java
@@ -43,6 +43,23 @@
    /** ç»Ÿè®¡æ‰˜ç›˜ç æ€»æ•°ï¼ˆmes_pro_pallet) */
    Long selectPalletCount();
    // ========== å‡ºåº“流通 / ä»“储 / è´¨é‡ ==========
    /** ç»Ÿè®¡å‘货通知单数(mes_wm_sales_notice) */
    Long selectSalesNoticeCount();
    /** ç»Ÿè®¡é”€å”®å‡ºåº“单数(mes_wm_product_sales) */
    Long selectProductSalesCount();
    /** ç»Ÿè®¡é”€å”®é€€è´§å•数(mes_wm_return_sales) */
    Long selectReturnSalesCount();
    /** ç»Ÿè®¡æœªå¤„理库存预警数(mes_wm_stock_warning.status=10) */
    Long selectStockWarningPendingCount();
    /** ç»Ÿè®¡å¾…评审不合格品单数(mes_qc_ncr.status=0) */
    Long selectNcrPendingCount();
    // ========== çŠ¶æ€åˆ†å¸ƒ ==========
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/bagcode/MesProBagCodeMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodePageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -36,6 +37,30 @@
    int markInbound(@org.apache.ibatis.annotations.Param("id") Long id, @org.apache.ibatis.annotations.Param("eventId") Long eventId,
                    @org.apache.ibatis.annotations.Param("deviceCode") String deviceCode, @org.apache.ibatis.annotations.Param("lineCode") String lineCode);
    /** æ‰¹æ¬¡æ•´æ‰¹å…¥åº“时批量标记其下未入库袋码(幂等),保持袋码与库存状态一致 */
    @Update("UPDATE mes_pro_bag_code SET inbound_time = NOW(), inbound_event_id = #{eventId}, inbound_line_code = #{lineCode}, update_time = NOW() "
            + "WHERE deleted = 0 AND inbound_event_id IS NULL AND batch_id = #{batchId}")
    int markInboundByBatchId(@Param("batchId") Long batchId, @Param("eventId") Long eventId,
                             @Param("lineCode") String lineCode);
    /** æ•´æ‰˜å…¥åº“时批量标记托盘下未入库袋码(幂等) */
    @Update("UPDATE mes_pro_bag_code SET inbound_time = NOW(), inbound_event_id = #{eventId}, inbound_line_code = #{lineCode}, update_time = NOW() "
            + "WHERE deleted = 0 AND inbound_event_id IS NULL AND pallet_id = #{palletId}")
    int markInboundByPalletId(@Param("palletId") Long palletId, @Param("eventId") Long eventId,
                              @Param("lineCode") String lineCode);
    /**
     * æ›´æ–°è¢‹ç è´¨æ£€çŠ¶æ€ï¼ˆç§å­è´¨æ£€åˆæ ¼/不合格时记录该袋结果)
     *
     * @param id       è¢‹ç ç¼–号
     * @param qcStatus è´¨æ£€çŠ¶æ€ï¼ˆè§ MesProBagCodeQcStatusEnum)
     */
    default void updateQcStatus(Long id, Integer qcStatus) {
        update(null, new LambdaUpdateWrapper<MesProBagCodeDO>()
                .eq(MesProBagCodeDO::getId, id)
                .set(MesProBagCodeDO::getQcStatus, qcStatus));
    }
    default List<MesProBagCodeDO> selectListByUuid(String uuid) {
        return selectList(MesProBagCodeDO::getUuid, uuid);
    }
@@ -46,6 +71,29 @@
    default List<MesProBagCodeDO> selectListByBatchId(Long batchId) {
        return selectList(MesProBagCodeDO::getBatchId, batchId);
    }
    /**
     * æŸ¥è¯¢ç”Ÿæˆç»„下可印刷的袋码(排除已作废),用于二维码标签导出
     * <p>
     * å·²ä½œåºŸè¢‹ç çš„æ ‡ç­¾ä¸å¾—再印刷:其标签若贴到产品上,消费者扫码会被提示「该追溯码已作废」,
     * å¯¼è‡´çœŸå“è¢«è¯¯åˆ¤ä¸ºå‡è´§ã€‚
     */
    default List<MesProBagCodeDO> selectPrintableListByUuid(String uuid) {
        return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>()
                .eq(MesProBagCodeDO::getUuid, uuid)
                .ne(MesProBagCodeDO::getStatus, MesProBagCodeStatusEnum.CANCELED.getStatus())
                .orderByAsc(MesProBagCodeDO::getBagNo));
    }
    /**
     * æŸ¥è¯¢æ‰¹æ¬¡ä¸‹å¯å°åˆ·çš„袋码(排除已作废),用于二维码标签导出
     */
    default List<MesProBagCodeDO> selectPrintableListByBatchId(Long batchId) {
        return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>()
                .eq(MesProBagCodeDO::getBatchId, batchId)
                .ne(MesProBagCodeDO::getStatus, MesProBagCodeStatusEnum.CANCELED.getStatus())
                .orderByAsc(MesProBagCodeDO::getBagNo));
    }
    default Long selectCountByBatchId(Long batchId) {
@@ -67,9 +115,11 @@
                .likeIfPresent(MesProBagCodeDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesProBagCodeDO::getUuid, reqVO.getUuid())
                .eqIfPresent(MesProBagCodeDO::getPalletId, reqVO.getPalletId())
                .eqIfPresent(MesProBagCodeDO::getItemId, reqVO.getItemId())
                .eqIfPresent(MesProBagCodeDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesProBagCodeDO::getCreateTime, reqVO.getCreateTime())
                .orderByAsc(MesProBagCodeDO::getBagNo));
                .orderByDesc(MesProBagCodeDO::getCreateTime)
                .orderByDesc(MesProBagCodeDO::getId));
    }
    /**
@@ -87,6 +137,18 @@
    }
    /**
     * æ‰˜ç›˜æ­£å¼ç ç”ŸæˆåŽï¼Œå›žå†™å…¶ä¸‹æ‰€æœ‰è¢‹ç çš„冗余托盘码
     *
     * @param palletId   æ‰˜ç›˜ç¼–号
     * @param palletCode æ­£å¼æ‰˜ç›˜ç 
     */
    default void updatePalletCodeByPalletId(Long palletId, String palletCode) {
        update(null, new LambdaUpdateWrapper<MesProBagCodeDO>()
                .set(MesProBagCodeDO::getPalletCode, palletCode)
                .eq(MesProBagCodeDO::getPalletId, palletId));
    }
    /**
     * æ‰¹é‡è§£ç»‘托盘(置空托盘编号与托盘码)
     *
     * @param ids è¢‹ç ç¼–号列表
@@ -99,19 +161,36 @@
    }
    /**
     * æŸ¥è¯¢æ‰¹æ¬¡å†…袋码最大序号(包含已逻辑删除记录,保证序号不回退、码值唯一)
     * æŸ¥è¯¢å…¨å±€æœ€å¤§è¢‹åºå·ï¼ˆè¢‹åºå·ä¸ºå…¨å±€åºå·ï¼Œä¸åŒºåˆ†æ‰¹æ¬¡ï¼›åŒ…含已逻辑删除记录,保证序号不回退)
     * <p>
     * ä»…用于为 Redis åºå·åˆ†é…æä¾›åŸºå‡†å€¼ï¼Œä¸è¦åœ¨ä¸šåŠ¡ä¸­ç›´æŽ¥ç”¨ã€Œè¿”å›žå€¼ + 1」作为起始序号——
     * é‚£æ˜¯å…ˆè¯»åŽå†™ï¼Œå¹¶å‘生成会产生重复序号。请使用
     * {@code MesCodeRedisDAO#allocateBagNoStart(int, Integer)} åŽŸå­åˆ†é…ã€‚
     *
     * @param batchId æ‰¹æ¬¡ç¼–号
     * @return æœ€å¤§è¢‹åºå·ï¼Œæ— è®°å½•返回 0
     * @return å…¨å±€æœ€å¤§è¢‹åºå·ï¼Œæ— è®°å½•返回 0
     */
    @Select("SELECT IFNULL(MAX(bag_no), 0) FROM mes_pro_bag_code")
    Integer selectMaxBagNo();
    @Update("<script>UPDATE mes_pro_bag_code SET batch_id = #{batchId}, batch_code = #{batchCode}, update_time = NOW() " +
            "WHERE deleted = 0 AND batch_id IS NULL AND status = #{status} AND id IN " +
            "WHERE deleted = 0 AND batch_id IS NULL AND status IN " +
            "<foreach collection='statuses' item='st' open='(' separator=',' close=')'>#{st}</foreach> AND id IN " +
            "<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
    int bindBatchByIds(@Param("ids") Collection<Long> ids, @Param("batchId") Long batchId,
                       @Param("batchCode") String batchCode, @Param("status") Integer status);
                       @Param("batchCode") String batchCode, @Param("statuses") Collection<Integer> statuses);
    /** æŸ¥è¯¢æ‰˜ç›˜ä¸‹å°šæœªç»‘定批次的袋码(托盘绑定批次时级联录入用) */
    default List<MesProBagCodeDO> selectUnboundListByPalletIds(Collection<Long> palletIds) {
        return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>()
                .in(MesProBagCodeDO::getPalletId, palletIds)
                .isNull(MesProBagCodeDO::getBatchId)
                .orderByAsc(MesProBagCodeDO::getId));
    }
    /** æ‰¹æ¬¡å†…有效袋码的品种编号集合(忽略历史空品种袋码),用于录入批次时的品种一致性校验 */
    @Select("SELECT DISTINCT item_id FROM mes_pro_bag_code " +
            "WHERE deleted = 0 AND status <> 30 AND batch_id = #{batchId} AND item_id IS NOT NULL")
    List<Long> selectDistinctItemIdsByBatchId(@Param("batchId") Long batchId);
    default List<MesProBagCodeDO> selectListByIdsForUpdate(Collection<Long> ids) {
        return selectList(new LambdaQueryWrapperX<MesProBagCodeDO>()
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/batch/MesProBatchMapper.java
@@ -40,14 +40,20 @@
    int markInbound(@org.apache.ibatis.annotations.Param("id") Long id, @org.apache.ibatis.annotations.Param("eventId") Long eventId,
                    @org.apache.ibatis.annotations.Param("deviceCode") String deviceCode, @org.apache.ibatis.annotations.Param("lineCode") String lineCode);
    /** æ‰¹æ¬¡æ•´æ‰¹æ‰«ç å…¥åº“回写:入库袋数直接置为批次袋数,幂等(已有入库标记的不再更新) */
    @org.apache.ibatis.annotations.Update("UPDATE mes_pro_batch SET inbound_time = NOW(), inbound_event_id = #{eventId}, inbound_line_code = #{lineCode}, inbound_bag_quantity = #{bagQuantity}, update_time = NOW() "
            + "WHERE id = #{id} AND deleted = 0 AND inbound_event_id IS NULL")
    int markBatchInbound(@org.apache.ibatis.annotations.Param("id") Long id, @org.apache.ibatis.annotations.Param("eventId") Long eventId,
                         @org.apache.ibatis.annotations.Param("lineCode") String lineCode, @org.apache.ibatis.annotations.Param("bagQuantity") Integer bagQuantity);
    default PageResult<MesProBatchDO> selectPage(MesProBatchPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesProBatchDO>()
                .likeIfPresent(MesProBatchDO::getCode, reqVO.getCode())
                .likeIfPresent(MesProBatchDO::getTempCode, reqVO.getTempCode())
                .eqIfPresent(MesProBatchDO::getItemId, reqVO.getItemId())
                .eqIfPresent(MesProBatchDO::getLineId, reqVO.getLineId())
                .likeIfPresent(MesProBatchDO::getLineCode, reqVO.getLineCode())
                .likeIfPresent(MesProBatchDO::getLineName, reqVO.getLineName())
                .likeIfPresent(MesProBatchDO::getProcessingTeam, reqVO.getProcessingTeam())
                .eqIfPresent(MesProBatchDO::getOwnerUserId, reqVO.getOwnerUserId())
                .eqIfPresent(MesProBatchDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesProBatchDO::getProduceDate, reqVO.getProduceDate())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/pallet/MesProPalletMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@@ -39,6 +40,15 @@
        return selectList(MesProPalletDO::getBatchId, batchId);
    }
    /** æŸ¥è¯¢æœªç»‘定生产批次的临时托盘(无正式托盘码且未作废),供批次新增/补录绑定托盘下拉使用 */
    default List<MesProPalletDO> selectUnboundList() {
        return selectList(new LambdaQueryWrapperX<MesProPalletDO>()
                .isNull(MesProPalletDO::getBatchId)
                .isNull(MesProPalletDO::getCode)
                .eq(MesProPalletDO::getStatus, MesProPalletStatusEnum.IN_USE.getStatus())
                .orderByAsc(MesProPalletDO::getId));
    }
    default PageResult<MesProPalletDO> selectPage(MesProPalletPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesProPalletDO>()
                .likeIfPresent(MesProPalletDO::getCode, reqVO.getCode())
@@ -64,6 +74,11 @@
    void markOutbound(@Param("id") Long id, @Param("salesId") Long salesId,
                      @Param("outboundTime") java.time.LocalDateTime outboundTime);
    /** æ•´æ‰˜æ‰«ç å…¥åº“回写:幂等(已有入库标记的不再更新),用于防重复入库 */
    @org.apache.ibatis.annotations.Update("UPDATE mes_pro_pallet SET inbound_event_id = #{eventId}, inbound_time = NOW(), update_time = NOW() "
            + "WHERE id = #{id} AND deleted = 0 AND inbound_event_id IS NULL")
    int markInbound(@Param("id") Long id, @Param("eventId") Long eventId);
    @org.apache.ibatis.annotations.Update("UPDATE mes_pro_pallet SET sales_id = NULL, outbound_time = NULL, update_time = NOW() "
            + "WHERE id = #{id} AND deleted = 0")
    void clearOutbound(@Param("id") Long id);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO;
import org.apache.ibatis.annotations.Mapper;
@@ -9,6 +11,15 @@
@Mapper
public interface MesQcSeedQualityMapper extends BaseMapperX<MesQcSeedQualityDO> {
    default PageResult<MesQcSeedQualityDO> selectPage(MesQcSeedQualityPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesQcSeedQualityDO>()
                .likeIfPresent(MesQcSeedQualityDO::getCode, reqVO.getCode())
                .likeIfPresent(MesQcSeedQualityDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesQcSeedQualityDO::getResult, reqVO.getResult())
                .orderByDesc(MesQcSeedQualityDO::getInspectedAt)
                .orderByDesc(MesQcSeedQualityDO::getId));
    }
    default MesQcSeedQualityDO selectByCode(String code) {
        return selectOne(new LambdaQueryWrapperX<MesQcSeedQualityDO>().eq(MesQcSeedQualityDO::getCode, code));
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
@@ -205,6 +206,14 @@
    List<MesWmBatchSalesTargetRespVO> selectSalesTargetList(String batchCode);
    /**
     * æŸ¥è¯¢æ‰¹æ¬¡å‡ºå…¥åº“流水(种子流向)
     *
     * @param batchCode æ‰¹æ¬¡å·
     * @return å‡ºå…¥åº“流水列表
     */
    List<MesWmBatchFlowRespVO> selectBatchFlowList(String batchCode);
    /**
     * æŸ¥è¯¢æ‰¹æ¬¡å·¥åºæµè½¬ï¼ˆäº§å‡º/投入事件)
     *
     * @param batchCode æ‰¹æ¬¡å·
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -22,13 +23,15 @@
    default PageResult<MesWmMaterialStockDO> selectPage(MesWmMaterialStockPageReqVO reqVO,
                                                         Collection<Long> itemTypeIds,
                                                         Collection<Long> itemIds,
                                                         Long virtualWarehouseId) {
                                                         Long virtualWarehouseId,
                                                         Collection<Long> warehouseIds) {
        LambdaQueryWrapperX<MesWmMaterialStockDO> wrapper = new LambdaQueryWrapperX<MesWmMaterialStockDO>()
                .inIfPresent(MesWmMaterialStockDO::getItemTypeId, itemTypeIds)
                .inIfPresent(MesWmMaterialStockDO::getItemId, itemIds)
                .likeIfPresent(MesWmMaterialStockDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesWmMaterialStockDO::getBatchId, reqVO.getBatchId())
                .eqIfPresent(MesWmMaterialStockDO::getWarehouseId, reqVO.getWarehouseId())
                .inIfPresent(MesWmMaterialStockDO::getWarehouseId, warehouseIds)
                .eqIfPresent(MesWmMaterialStockDO::getLocationId, reqVO.getLocationId())
                .eqIfPresent(MesWmMaterialStockDO::getAreaId, reqVO.getAreaId())
                .eqIfPresent(MesWmMaterialStockDO::getVendorId, reqVO.getVendorId())
@@ -149,9 +152,10 @@
    }
    /**
     * æŒ‰ç»„合键查询库存记录(忽略供应商维度)
     * æŒ‰ç»„合键查询库存记录(忽略供应商维度,仅合格库)
     *
     * ç”¨äºŽç§»åº“场景:移库时将库存合并到目标库位已有库存,不区分供应商
     * ç”¨äºŽç§»åº“场景:移库时将合格库存合并到目标库位已有合格库存,不区分供应商
     * åªåŒ¹é…åˆæ ¼åº“(20),避免将移入数量合并到同批次的暂存库记录
     */
    default MesWmMaterialStockDO selectByCompositeKeyIgnoreVendor(Long itemId, Long warehouseId, Long locationId,
                                                                    Long areaId, Long batchId) {
@@ -159,7 +163,8 @@
                .eq(MesWmMaterialStockDO::getItemId, itemId)
                .eq(MesWmMaterialStockDO::getWarehouseId, warehouseId)
                .eq(MesWmMaterialStockDO::getLocationId, locationId)
                .eq(MesWmMaterialStockDO::getAreaId, areaId);
                .eq(MesWmMaterialStockDO::getAreaId, areaId)
                .eq(MesWmMaterialStockDO::getStockState, MesWmStockStateEnum.QUALIFIED.getState());
        // batchId=null æ—¶ç²¾ç¡®åŒ¹é… is null
        if (batchId != null) {
            wrapper.eq(MesWmMaterialStockDO::getBatchId, batchId);
@@ -170,9 +175,41 @@
        return selectOne(wrapper);
    }
    /**
     * æŒ‰ç»„合键查询合格库库存记录(用于出库事务匹配)
     *
     * åŒæ‰¹æ¬¡å¯èƒ½åŒæ—¶å­˜åœ¨æš‚存库(10)与合格库(20)两条记录(种子质检按袋拆数量),
     * å‡ºåº“必须只从合格库匹配,避免扣到未质检的暂存库存。
     */
    default MesWmMaterialStockDO selectQualifiedByCompositeKey(Long itemId, Long warehouseId, Long locationId,
                                                                 Long areaId, Long batchId, Long vendorId) {
        LambdaQueryWrapperX<MesWmMaterialStockDO> wrapper = new LambdaQueryWrapperX<MesWmMaterialStockDO>()
                .eq(MesWmMaterialStockDO::getItemId, itemId)
                .eq(MesWmMaterialStockDO::getWarehouseId, warehouseId)
                .eq(MesWmMaterialStockDO::getLocationId, locationId)
                .eq(MesWmMaterialStockDO::getAreaId, areaId)
                .eq(MesWmMaterialStockDO::getStockState, MesWmStockStateEnum.QUALIFIED.getState());
        // batchId=null æ—¶ç²¾ç¡®åŒ¹é… is null
        if (batchId != null) {
            wrapper.eq(MesWmMaterialStockDO::getBatchId, batchId);
        } else {
            wrapper.isNull(MesWmMaterialStockDO::getBatchId);
        }
        if (vendorId != null) {
            wrapper.eq(MesWmMaterialStockDO::getVendorId, vendorId);
        }
        wrapper.last("ORDER BY quantity DESC LIMIT 1");
        return selectOne(wrapper);
    }
    default List<MesWmMaterialStockDO> selectList(MesWmMaterialStockListReqVO reqVO) {
        return selectList(reqVO, null);
    }
    default List<MesWmMaterialStockDO> selectList(MesWmMaterialStockListReqVO reqVO, Collection<Long> warehouseIds) {
        return selectList(new LambdaQueryWrapperX<MesWmMaterialStockDO>()
                .eqIfPresent(MesWmMaterialStockDO::getWarehouseId, reqVO.getWarehouseId())
                .inIfPresent(MesWmMaterialStockDO::getWarehouseId, warehouseIds)
                .eqIfPresent(MesWmMaterialStockDO::getLocationId, reqVO.getLocationId())
                .eqIfPresent(MesWmMaterialStockDO::getAreaId, reqVO.getAreaId())
                .eqIfPresent(MesWmMaterialStockDO::getItemId, reqVO.getItemId())
@@ -197,6 +234,36 @@
                .orderByAsc(MesWmMaterialStockDO::getReceiptTime));
    }
    /**
     * æŒ‰ç‰©æ–™/批次查询合格库库存记录(仅 stock_state=20,且仅限合格仓库)
     *
     * <p>同批次可能同时存在暂存库(10)与合格库(20)两条记录(种子质检按袋拆数量),
     * é”€å”®å‡ºåº“选库存只能从合格库记录中选,杜绝选中未质检的暂存库存;
     * åŒæ—¶é™å®šåˆæ ¼ä»“库(type=20),避免从暂存仓库中误选合格态库存。</p>
     */
    default List<MesWmMaterialStockDO> selectQualifiedListByItemAndBatch(Long itemId, Long batchId,
                                                                          Collection<Long> qualifiedWarehouseIds) {
        return selectList(new LambdaQueryWrapperX<MesWmMaterialStockDO>()
                .eq(MesWmMaterialStockDO::getItemId, itemId)
                .eqIfPresent(MesWmMaterialStockDO::getBatchId, batchId)
                .eq(MesWmMaterialStockDO::getStockState, MesWmStockStateEnum.QUALIFIED.getState())
                .in(MesWmMaterialStockDO::getWarehouseId, qualifiedWarehouseIds)
                .gt(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO)
                .orderByAsc(MesWmMaterialStockDO::getReceiptTime));
    }
    /**
     * æŒ‰ä»“库批次查询全部库存记录(含暂存库与合格库,quantity>0)
     *
     * <p>用于种子质检按袋码流转:定位该批次暂存库记录扣减 1 è¢‹ï¼Œä»¥åŠå®šä½/新建合格库记录累加 1 è¢‹ã€‚</p>
     */
    default List<MesWmMaterialStockDO> selectStockListByBatchId(Long batchId) {
        return selectList(new LambdaQueryWrapperX<MesWmMaterialStockDO>()
                .eq(MesWmMaterialStockDO::getBatchId, batchId)
                .gt(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO)
                .orderByAsc(MesWmMaterialStockDO::getReceiptTime));
    }
    default List<MesWmMaterialStockDO> selectListByItemIds(Collection<Long> itemIds) {
        if (itemIds == null || itemIds.isEmpty()) {
            return java.util.Collections.emptyList();
@@ -207,18 +274,16 @@
    }
    /**
     * æŒ‰æ‰¹æ¬¡å·æ›´æ–°åº“存状态(用于种子质检合格后,将暂存库整批转为合格库)
     * ç»Ÿè®¡æ‰¹æ¬¡åœ¨æš‚存库中的库存记录数(用于种子质检前校验袋码/批次是否处于待质检状态)
     *
     * @param batchId   ä»“库批次ID
     * @param toState   ç›®æ ‡åº“存状态
     * @param fromState åŽŸåº“å­˜çŠ¶æ€ï¼ˆé™å®šåªæ›´æ–°è¯¥çŠ¶æ€çš„è®°å½•ï¼‰
     * @return å½±å“è¡Œæ•°
     * @param batchId ä»“库批次ID
     * @return æš‚存库库存记录数
     */
    default int updateStockStateByBatchId(Long batchId, Integer toState, Integer fromState) {
        return update(new MesWmMaterialStockDO().setStockState(toState),
                new LambdaUpdateWrapper<MesWmMaterialStockDO>()
                        .eq(MesWmMaterialStockDO::getBatchId, batchId)
                        .eq(MesWmMaterialStockDO::getStockState, fromState));
    default Long selectTempStockCountByBatchId(Long batchId) {
        return selectCount(new LambdaQueryWrapperX<MesWmMaterialStockDO>()
                .eq(MesWmMaterialStockDO::getBatchId, batchId)
                .eq(MesWmMaterialStockDO::getStockState, MesWmStockStateEnum.TEMP.getState())
                .gt(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstockbag/MesWmMaterialStockBagMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockBagPageReqVO;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface MesWmMaterialStockBagMapper extends BaseMapperX<MesWmMaterialStockBagDO> {
    default PageResult<MesWmMaterialStockBagDO> selectPage(MesWmMaterialStockBagPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmMaterialStockBagDO>()
                .eq(MesWmMaterialStockBagDO::getMaterialStockId, reqVO.getMaterialStockId())
                .likeIfPresent(MesWmMaterialStockBagDO::getBagCode, reqVO.getBagCode())
                .eq(MesWmMaterialStockBagDO::getStatus, 1)
                .orderByAsc(MesWmMaterialStockBagDO::getInboundTime));
    }
    default MesWmMaterialStockBagDO selectByBagCode(String bagCode) {
        return selectOne(new LambdaQueryWrapperX<MesWmMaterialStockBagDO>()
                .eq(MesWmMaterialStockBagDO::getBagCode, bagCode)
                .orderByDesc(MesWmMaterialStockBagDO::getId)
                .last("LIMIT 1"));
    }
    default MesWmMaterialStockBagDO selectByBagCodeId(Long bagCodeId) {
        return selectOne(new LambdaQueryWrapperX<MesWmMaterialStockBagDO>()
                .eq(MesWmMaterialStockBagDO::getBagCodeId, bagCodeId)
                .orderByDesc(MesWmMaterialStockBagDO::getId)
                .last("LIMIT 1"));
    }
    default MesWmMaterialStockBagDO selectOneActiveByMaterialStockId(Long materialStockId) {
        return selectOne(new LambdaQueryWrapperX<MesWmMaterialStockBagDO>()
                .eq(MesWmMaterialStockBagDO::getMaterialStockId, materialStockId)
                .eq(MesWmMaterialStockBagDO::getStatus, 1)
                .last("LIMIT 1"));
    }
    default MesWmMaterialStockBagDO selectActiveByBagCode(String bagCode) {
        return selectOne(new LambdaQueryWrapperX<MesWmMaterialStockBagDO>()
                .eq(MesWmMaterialStockBagDO::getBagCode, bagCode)
                .eq(MesWmMaterialStockBagDO::getStatus, 1)
                .last("LIMIT 1"));
    }
    default MesWmMaterialStockBagDO selectActiveByBagCodeId(Long bagCodeId) {
        return selectOne(new LambdaQueryWrapperX<MesWmMaterialStockBagDO>()
                .eq(MesWmMaterialStockBagDO::getBagCodeId, bagCodeId)
                .eq(MesWmMaterialStockBagDO::getStatus, 1)
                .last("LIMIT 1"));
    }
    default List<MesWmMaterialStockBagDO> selectActiveListByItemAndBatch(Long itemId, Long batchId, Integer stockState) {
        return selectList(new LambdaQueryWrapperX<MesWmMaterialStockBagDO>()
                .eq(MesWmMaterialStockBagDO::getItemId, itemId)
                .eqIfPresent(MesWmMaterialStockBagDO::getBatchId, batchId)
                .eqIfPresent(MesWmMaterialStockBagDO::getStockState, stockState)
                .eq(MesWmMaterialStockBagDO::getStatus, 1)
                .eq(MesWmMaterialStockBagDO::getFrozen, false)
                .orderByAsc(MesWmMaterialStockBagDO::getInboundTime));
    }
    default List<MesWmMaterialStockBagDO> selectListByMaterialStockIds(Collection<Long> materialStockIds) {
        return selectList(new LambdaQueryWrapperX<MesWmMaterialStockBagDO>()
                .in(MesWmMaterialStockBagDO::getMaterialStockId, materialStockIds)
                .eq(MesWmMaterialStockBagDO::getStatus, 1)
                .orderByAsc(MesWmMaterialStockBagDO::getInboundTime));
    }
    default List<MesWmMaterialStockBagDO> selectListByMaterialStockId(Long materialStockId) {
        return selectList(new LambdaQueryWrapperX<MesWmMaterialStockBagDO>()
                .eq(MesWmMaterialStockBagDO::getMaterialStockId, materialStockId)
                .eq(MesWmMaterialStockBagDO::getStatus, 1)
                .orderByAsc(MesWmMaterialStockBagDO::getInboundTime));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesLineMapper.java
@@ -7,7 +7,6 @@
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Map;
/**
 * MES é”€å”®å‡ºåº“单行 Mapper
@@ -31,26 +30,6 @@
                .eqIfPresent(MesWmProductSalesLineDO::getSalesId, reqVO.getSalesId())
                .inIfPresent(MesWmProductSalesLineDO::getSalesId, reqVO.getSalesIds())
                .orderByDesc(MesWmProductSalesLineDO::getId));
    }
    /**
     * æ ¹æ®é”€å”®è®¢å•明细 ID åˆ—表查询出库行,按 saleOrderItemId åˆ†ç»„返回批号
     */
    default Map<Long, String> selectBatchCodesBySaleOrderItemIds(java.util.Collection<Long> saleOrderItemIds) {
        if (saleOrderItemIds == null || saleOrderItemIds.isEmpty()) {
            return java.util.Collections.emptyMap();
        }
        List<MesWmProductSalesLineDO> lines = selectList(new LambdaQueryWrapperX<MesWmProductSalesLineDO>()
                .in(MesWmProductSalesLineDO::getSaleOrderItemId, saleOrderItemIds));
        return lines.stream()
                .filter(l -> l.getBatchCode() != null && !l.getBatchCode().isEmpty())
                .collect(java.util.stream.Collectors.groupingBy(
                        MesWmProductSalesLineDO::getSaleOrderItemId,
                        java.util.LinkedHashMap::new,
                        java.util.stream.Collectors.collectingAndThen(
                                java.util.stream.Collectors.mapping(MesWmProductSalesLineDO::getBatchCode,
                                        java.util.stream.Collectors.toSet()),
                                codes -> String.join("、", codes))));
    }
    default void updateQualityStatusByIds(List<Long> ids, Integer qualityStatus) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/returnsales/MesWmReturnSalesMapper.java
@@ -22,7 +22,6 @@
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmReturnSalesDO>()
                .likeIfPresent(MesWmReturnSalesDO::getCode, reqVO.getCode())
                .likeIfPresent(MesWmReturnSalesDO::getName, reqVO.getName())
                .likeIfPresent(MesWmReturnSalesDO::getSalesOrderCode, reqVO.getSalesOrderCode())
                .eqIfPresent(MesWmReturnSalesDO::getClientId, reqVO.getClientId())
                .eqIfPresent(MesWmReturnSalesDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesWmReturnSalesDO::getReturnDate, reqVO.getReturnDate())
@@ -36,7 +35,6 @@
        LambdaQueryWrapperX<MesWmReturnSalesDO> wrapper = new LambdaQueryWrapperX<MesWmReturnSalesDO>()
                .likeIfPresent(MesWmReturnSalesDO::getCode, reqVO.getCode())
                .likeIfPresent(MesWmReturnSalesDO::getName, reqVO.getName())
                .likeIfPresent(MesWmReturnSalesDO::getSalesOrderCode, reqVO.getSalesOrderCode())
                .eqIfPresent(MesWmReturnSalesDO::getClientId, reqVO.getClientId())
                .eqIfPresent(MesWmReturnSalesDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesWmReturnSalesDO::getReturnDate, reqVO.getReturnDate())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/salesnotice/MesWmSalesNoticeMapper.java
@@ -8,7 +8,6 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
@@ -21,7 +20,6 @@
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmSalesNoticeDO>()
                .likeIfPresent(MesWmSalesNoticeDO::getCode, reqVO.getCode())
                .likeIfPresent(MesWmSalesNoticeDO::getName, reqVO.getName())
                .likeIfPresent(MesWmSalesNoticeDO::getSaleOrderCode, reqVO.getSaleOrderCode())
                .eqIfPresent(MesWmSalesNoticeDO::getClientId, reqVO.getClientId())
                .eqIfPresent(MesWmSalesNoticeDO::getStatus, reqVO.getStatus())
                .orderByDesc(MesWmSalesNoticeDO::getId));
@@ -38,7 +36,6 @@
        LambdaQueryWrapperX<MesWmSalesNoticeDO> wrapper = new LambdaQueryWrapperX<MesWmSalesNoticeDO>()
                .likeIfPresent(MesWmSalesNoticeDO::getCode, reqVO.getCode())
                .likeIfPresent(MesWmSalesNoticeDO::getName, reqVO.getName())
                .likeIfPresent(MesWmSalesNoticeDO::getSaleOrderCode, reqVO.getSaleOrderCode())
                .eqIfPresent(MesWmSalesNoticeDO::getClientId, reqVO.getClientId())
                .eqIfPresent(MesWmSalesNoticeDO::getStatus, reqVO.getStatus())
                .orderByDesc(MesWmSalesNoticeDO::getId);
@@ -55,19 +52,6 @@
    default MesWmSalesNoticeDO selectByCode(String code) {
        return selectOne(MesWmSalesNoticeDO::getCode, code);
    }
    default MesWmSalesNoticeDO selectBySaleOrderId(Long saleOrderId) {
        return selectOne(MesWmSalesNoticeDO::getSaleOrderId, saleOrderId);
    }
    /**
     * æŸ¥è¯¢å·²å­˜åœ¨å‘货通知单的销售订单 ID åˆ—表
     */
    default List<Long> selectExistingSaleOrderIds(Collection<Long> saleOrderIds) {
        return selectObjs(new LambdaQueryWrapperX<MesWmSalesNoticeDO>()
                .select(MesWmSalesNoticeDO::getSaleOrderId)
                .in(MesWmSalesNoticeDO::getSaleOrderId, saleOrderIds));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java
@@ -17,6 +17,7 @@
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmTransactionDO>()
                .eqIfPresent(MesWmTransactionDO::getItemId, reqVO.getItemId())
                .likeIfPresent(MesWmTransactionDO::getBatchCode, reqVO.getBatchCode())
                .likeIfPresent(MesWmTransactionDO::getBagCode, reqVO.getBagCode())
                .eqIfPresent(MesWmTransactionDO::getWarehouseId, reqVO.getWarehouseId())
                .eqIfPresent(MesWmTransactionDO::getLocationId, reqVO.getLocationId())
                .eqIfPresent(MesWmTransactionDO::getAreaId, reqVO.getAreaId())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/warehouse/MesWmWarehouseMapper.java
@@ -21,9 +21,16 @@
                .likeIfPresent(MesWmWarehouseDO::getCode, reqVO.getCode())
                .likeIfPresent(MesWmWarehouseDO::getName, reqVO.getName())
                .eqIfPresent(MesWmWarehouseDO::getFrozen, reqVO.getFrozen())
                .eqIfPresent(MesWmWarehouseDO::getType, reqVO.getType())
                .orderByDesc(MesWmWarehouseDO::getId));
    }
    default List<MesWmWarehouseDO> selectListByType(Integer type) {
        return selectList(new LambdaQueryWrapperX<MesWmWarehouseDO>()
                .eq(MesWmWarehouseDO::getType, type)
                .orderByAsc(MesWmWarehouseDO::getId));
    }
    default MesWmWarehouseDO selectByCode(String code) {
        return selectOne(MesWmWarehouseDO::getCode, code);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/MesCodeRedisDAO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.mes.dal.redis;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
 * MES ä¸šåŠ¡ç¼–ç ç”Ÿæˆçš„ Redis DAO(实例码:袋码 / æ‰˜ç›˜ç  / ä¸´æ—¶ç ç­‰ï¼‰ã€‚
 *
 * <p>需求:所有业务编码统一为「16 ä½çº¯æ•°å­—」,且全系统唯一。
 * è¿™é‡Œå¤ç”¨å…¨ç³»ç»Ÿç»Ÿä¸€çš„ Redis è‡ªå¢ž KEY,保证与其它模块的编码也不重复。
 */
@Repository
public class MesCodeRedisDAO {
    /**
     * è¢‹åºå·è‡ªå¢ž KEY(全局袋序号,保证同一批/跨批生成时不重复)
     */
    private static final String BAG_NO_SEQ_KEY = "mes:bag_no:seq";
    /**
     * åŽŸå­åˆ†é…è¢‹åºå·åŒºé—´çš„ Lua è„šæœ¬ã€‚
     * <p>
     * å…ˆæŒ‰åº“内最大序号校正基准值(Redis é¦–次使用、或库内数据被直接导入导致基准落后时),
     * å†åŽŸå­è‡ªå¢žå¹¶è¿”å›žè‡ªå¢žåŽçš„å€¼ã€‚æ•´ä¸ªã€Œæ ¡æ­£ + è‡ªå¢žã€åœ¨ Redis å•线程内完成,避免并发下互相覆盖计数。
     * <p>
     * KEYS[1] = åºå· KEY;ARGV[1] = æœ¬æ¬¡éœ€è¦çš„个数;ARGV[2] = åº“内当前最大序号
     */
    private static final RedisScript<Long> ALLOCATE_BAG_NO_SCRIPT = new DefaultRedisScript<>(
            "local cur = redis.call('GET', KEYS[1]) "
                    + "local base = tonumber(ARGV[2]) "
                    + "if (not cur) or (tonumber(cur) < base) then redis.call('SET', KEYS[1], base) end "
                    + "return redis.call('INCRBY', KEYS[1], ARGV[1])",
            Long.class);
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆä¸€ä¸ª 16 ä½çº¯æ•°å­—业务编码(全系统唯一)
     *
     * @return 16 ä½çº¯æ•°å­—编码
     */
    public String generate() {
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
    /**
     * åŽŸå­åˆ†é…ä¸€æ®µè¿žç»­çš„è¢‹åºå·ï¼Œè¿”å›žåŒºé—´èµ·å§‹å€¼ã€‚
     * <p>
     * è¢‹åºå·åŽŸå…ˆç”±ã€ŒæŸ¥åº“å†…æœ€å¤§å€¼å† +1」得到,并发生成时两个请求会读到同一个最大值,
     * ä»Žè€Œäº§å‡ºé‡å¤çš„袋序号(袋序号会印在标签上)。此处改为 Redis åŽŸå­è‡ªå¢žåˆ†é…ï¼Œ
     * å¹¶ç”¨åº“内当前最大值做基准校正,保证序号既不重复也不回退。
     *
     * @param count      æœ¬æ¬¡éœ€è¦çš„连续序号个数(须大于 0)
     * @param dbMaxBagNo æ•°æ®åº“当前最大袋序号(含已逻辑删除记录,传 null è§†ä¸º 0)
     * @return åŒºé—´èµ·å§‹åºå·ï¼ˆå«ï¼‰ï¼Œå¯ç”¨åŒºé—´ä¸º [start, start + count - 1]
     */
    public int allocateBagNoStart(int count, Integer dbMaxBagNo) {
        if (count <= 0) {
            throw new IllegalArgumentException("袋序号分配个数必须大于 0");
        }
        Long end = stringRedisTemplate.execute(ALLOCATE_BAG_NO_SCRIPT, List.of(BAG_NO_SEQ_KEY),
                String.valueOf(count), String.valueOf(dbMaxBagNo == null ? 0 : dbMaxBagNo));
        if (end == null) {
            throw new IllegalStateException("分配袋序号失败,请重试");
        }
        return (int) (end - count + 1);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -455,6 +455,8 @@
    ErrorCode WM_WAREHOUSE_HAS_WORKSTATION = new ErrorCode(1_040_700_004, "仓库已被工作站引用,无法删除");
    ErrorCode WM_WAREHOUSE_HAS_MATERIAL_STOCK = new ErrorCode(1_040_700_005, "仓库下有库存记录,无法删除");
    ErrorCode WM_WAREHOUSE_IS_VIRTUAL = new ErrorCode(1_040_700_006, "虚拟仓库不允许操作");
    ErrorCode WM_WAREHOUSE_TYPE_NOT_QUALIFIED = new ErrorCode(1_040_700_007, "所选仓库不是合格库");
    ErrorCode WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED = new ErrorCode(1_040_700_008, "未配置合格库,请先在仓库管理中创建并设置仓库类型为合格库");
    // ========== MES ä»“库管理-库区(1-040-701-000) ==========
    ErrorCode WM_WAREHOUSE_LOCATION_NOT_EXISTS = new ErrorCode(1_040_701_000, "库区不存在");
@@ -496,6 +498,12 @@
    ErrorCode WM_MATERIAL_STOCK_SELECTION_MISMATCH = new ErrorCode(1_040_703_014, "库存记录与提交的物料、批次或库位信息不一致");
    ErrorCode WM_MATERIAL_STOCK_QUANTITY_INVALID = new ErrorCode(1_040_703_015, "调整数量不能为空或零");
    ErrorCode WM_MATERIAL_STOCK_FROZEN = new ErrorCode(1_040_703_016, "库存记录已冻结,无法移库");
    ErrorCode WM_MATERIAL_STOCK_NOT_QUALIFIED = new ErrorCode(1_040_703_017, "非合格库存不可出库");
    ErrorCode WM_MATERIAL_STOCK_BAG_NOT_EXISTS = new ErrorCode(1_040_703_018, "库存袋码明细不存在");
    ErrorCode WM_MATERIAL_STOCK_BAG_DUPLICATE = new ErrorCode(1_040_703_019, "袋码({})已在库,禁止重复入库");
    ErrorCode WM_MATERIAL_STOCK_BAG_NOT_ACTIVE = new ErrorCode(1_040_703_020, "袋码({})不在库或已出库/报废,无法操作");
    ErrorCode WM_MATERIAL_STOCK_BAG_REQUIRED = new ErrorCode(1_040_703_021, "该物料启用袋码管理,必须携带袋码");
    ErrorCode WM_MATERIAL_STOCK_BAG_INSUFFICIENT = new ErrorCode(1_040_703_022, "在库袋码明细数量不足,无法出库");
    // ========== MES ä»“库管理-到货通知单(1-040-704-000) ==========
    ErrorCode WM_ARRIVAL_NOTICE_NOT_EXISTS = new ErrorCode(1_040_704_000, "到货通知单不存在");
@@ -727,6 +735,8 @@
    ErrorCode WM_PRODUCT_SALES_SCAN_STOCK_NOT_FOUND = new ErrorCode(1_040_714_105, "扫描批次在合格库中未找到对应库存记录");
    ErrorCode WM_PRODUCT_SALES_SCAN_NO_MATCH_LINE = new ErrorCode(1_040_714_106, "出库单中没有与该物料/批次匹配的行");
    ErrorCode WM_PRODUCT_SALES_SCAN_BAG_ALREADY_SCANNED = new ErrorCode(1_040_714_107, "该袋码已扫描出库,请勿重复扫描");
    ErrorCode WM_PRODUCT_SALES_SCAN_BAG_NOT_QUALIFIED = new ErrorCode(1_040_714_108, "该袋码未质检合格,禁止出库");
    ErrorCode WM_PRODUCT_SALES_SCAN_PALLET_NOT_ALL_QUALIFIED = new ErrorCode(1_040_714_109, "托盘下存在未质检袋码,禁止整托出库");
    // ========== MES ä»“库管理-杂项出库单(1-040-715-000) ==========
    ErrorCode WM_MISC_ISSUE_NOT_EXISTS = new ErrorCode(1_040_715_000, "杂项出库单不存在");
@@ -760,9 +770,6 @@
    ErrorCode WM_SALES_NOTICE_LINE_NOT_EXISTS = new ErrorCode(1_040_720_010, "发货通知单行不存在");
    ErrorCode WM_SALES_NOTICE_LINE_EMPTY = new ErrorCode(1_040_720_011, "发货通知单行为空,不能提交");
    ErrorCode WM_SALES_NOTICE_LINE_NOT_MATCH = new ErrorCode(1_040_720_012, "发货通知单行不属于指定的发货通知单");
    ErrorCode WM_SALES_NOTICE_SALE_ORDER_NOT_EXISTS = new ErrorCode(1_040_720_013, "销售订单不存在");
    ErrorCode WM_SALES_NOTICE_SALE_ORDER_ITEM_EMPTY = new ErrorCode(1_040_720_014, "销售订单明细为空");
    ErrorCode WM_SALES_NOTICE_ALREADY_EXISTS = new ErrorCode(1_040_720_015, "该销售订单已生成发货通知单,不能重复生成");
    // ========== MES ä»“库管理-条码配置(1-040-730-000) ==========
    ErrorCode WM_BARCODE_CONFIG_NOT_EXISTS = new ErrorCode(1_040_730_000, "条码配置不存在");
@@ -844,12 +851,21 @@
    ErrorCode PRO_BAG_CODE_BIND_PALLET_MISMATCH = new ErrorCode(1_040_752_003, "袋码与托盘不属于同一生产批次,不允许绑定");
    ErrorCode PRO_BAG_CODE_BIND_PALLET_BOUND = new ErrorCode(1_040_752_004, "存在已绑定托盘的袋码,请先解绑");
    ErrorCode PRO_BAG_CODE_UUID_NOT_EXISTS = new ErrorCode(1_040_752_005, "袋码生成组不存在");
    ErrorCode PRO_BAG_CODE_BIND_PALLET_OVER_CAPACITY = new ErrorCode(1_040_752_006, "绑定后袋码数量超过托盘可装袋数");
    ErrorCode PRO_BAG_CODE_REPLACE_BATCH_STATUS_INVALID = new ErrorCode(1_040_752_007, "生产批次状态不允许破损袋码替换");
    ErrorCode PRO_BAG_CODE_REPLACE_BATCH_MISMATCH = new ErrorCode(1_040_752_008, "待替换袋码不属于指定生产批次");
    ErrorCode PRO_BAG_CODE_REPLACE_ALREADY_REPLACED = new ErrorCode(1_040_752_009, "袋码已完成替换,不允许重复替换");
    ErrorCode PRO_BAG_CODE_REPLACE_STATUS_INVALID = new ErrorCode(1_040_752_010, "袋码状态不允许破损替换");
    ErrorCode PRO_BAG_CODE_REPLACE_PALLET_INVALID = new ErrorCode(1_040_752_011, "袋码原托盘不存在或已作废,不允许继承");
    ErrorCode PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED = new ErrorCode(1_040_752_012, "品种主计量单位未配置类型,仅支持 48千克/100千克/吨");
    ErrorCode PRO_BAG_CODE_PALLET_COUNT_REQUIRED = new ErrorCode(1_040_752_013, "该品种按托盘生成袋码,托盘数必须大于 0");
    ErrorCode PRO_BAG_CODE_BIND_BATCH_ITEM_MISMATCH = new ErrorCode(1_040_752_014, "录入批次的袋码必须属于同一品种");
    // ========== MES ç§å­æ ‡ç­¾å›½æ ‡å››è¦ç´ ï¼ˆ1-040-750-020) ==========
    ErrorCode PRO_QR_LABEL_TRACE_URL_NOT_CONFIGURED = new ErrorCode(1_040_750_020, "未配置追溯网址,无法生成二维码标签");
    ErrorCode PRO_QR_LABEL_ITEM_NAME_MISSING = new ErrorCode(1_040_750_021, "标签缺少品种名称,无法导出");
    ErrorCode PRO_QR_LABEL_OPERATOR_MISSING = new ErrorCode(1_040_750_022, "标签缺少生产经营者名称,无法导出");
    ErrorCode PRO_QR_LABEL_UNIT_CODE_MISSING = new ErrorCode(1_040_750_023, "标签缺少单元识别代码,无法导出");
    ErrorCode PRO_QR_LABEL_UNIT_CODE_INVALID = new ErrorCode(1_040_750_024, "单元识别代码不符合国标:长度不超过 20 ä¸ªå­—符,且只能为数字或字母数字组合");
    // ========== MES ç”Ÿäº§ç®¡ç†-托盘码(1-040-753-000) ==========
    ErrorCode PRO_PALLET_NOT_EXISTS = new ErrorCode(1_040_753_000, "托盘码不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesBizTypeConstants.java
@@ -18,6 +18,7 @@
    public static final int QC_IPQC = 2; // è¿‡ç¨‹æ£€éªŒï¼šMesQcIpqcDO
    public static final int QC_OQC = 3; // å‡ºè´§æ£€éªŒï¼šMesQcOqcDO
    public static final int QC_RQC = 4; // é€€è´§æ£€éªŒï¼šMesQcRqcDO
    public static final int QC_SEED_QUALITY = 5; // ç§å­è´¨æ£€ï¼šMesQcSeedQualityDO(含暂存出库/合格入库/报废)
    // ========== WM ä»“库模块 [100, 200) ==========
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pro/MesProBagCodeQcStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.mes.enums.pro;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES è¢‹ç ï¼ˆä¸€è¢‹ä¸€ç ï¼‰è´¨æ£€çŠ¶æ€æžšä¸¾
 *
 * <p>记录每袋的种子质检结果,销售出库扫袋码时校验该袋已质检合格。</p>
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesProBagCodeQcStatusEnum implements ArrayValuable<Integer> {
    UNCHECKED(10, "未质检"),
    QUALIFIED(20, "质检合格"),
    UNQUALIFIED(30, "质检不合格");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesProBagCodeQcStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€
     */
    private final Integer status;
    /**
     * çŠ¶æ€åç§°
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/trace/MesTraceCodeStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.enums.trace;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * è¿½æº¯ç æœ‰æ•ˆæ€§çŠ¶æ€æžšä¸¾ï¼ˆå…¬å¼€æº¯æºæŸ¥è¯¢ç»“æžœï¼‰
 * <p>
 * ä¾æ®ã€Šå†œä½œç‰©ç§å­æ ‡ç­¾äºŒç»´ç ç¼–码规则》,每个二维码对应唯一的最小销售单元且不得重复使用。
 * å·²ä½œåºŸçš„袋码(破损替换后作废)必须对外失效,否则作废码仍可被冒用销售,
 * å› æ­¤æº¯æºæŸ¥è¯¢éœ€æ˜Žç¡®åŒºåˆ†ã€Œæœ‰æ•ˆã€ä¸Žã€Œå·²ä½œåºŸã€ã€‚
 */
@Getter
@AllArgsConstructor
public enum MesTraceCodeStatusEnum {
    /**
     * æœ‰æ•ˆï¼šç å¤„于正常流转状态
     */
    VALID("VALID", "有效"),
    /**
     * å·²ä½œåºŸï¼šè¢‹ç å·²è¢«ç ´æŸæ›¿æ¢æµç¨‹ä½œåºŸï¼Œä¸å¾—作为有效产品流通
     */
    CANCELED("CANCELED", "已作废");
    /**
     * çŠ¶æ€ç¼–ç ï¼ˆå¯¹å¤–è¾“å‡ºï¼‰
     */
    private final String status;
    /**
     * çŠ¶æ€åç§°
     */
    private final String name;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmMaterialStockBagStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.enums.wm;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum MesWmMaterialStockBagStatusEnum {
    ACTIVE(1, "在库"),
    OUT(2, "已出库"),
    SCRAPPED(3, "已报废");
    private final Integer status;
    private final String name;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmWarehouseTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.enums.wm;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MES ä»“库类型
 *
 * ä¸Ž {@link MesWmStockStateEnum} è¯­ä¹‰å¯¹é½ï¼šæš‚存库存放待检/暂存库存,合格库存放质检合格、可拣货库存。
 */
@Getter
@AllArgsConstructor
public enum MesWmWarehouseTypeEnum {
    TEMP(10, "暂存库"),
    QUALIFIED(20, "合格库");
    private final Integer type;
    private final String name;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/config/MesProperties.java
@@ -14,8 +14,36 @@
    /**
     * H5 æº¯æºé¡µé¢åœ°å€ï¼ˆtrace.html)基础 URL,用于生成携带追溯码参数的二维码内容。
     * ä¸ºç©ºæ—¶äºŒç»´ç ä»ç¼–码纯码值(兼容旧码)。
     * <p>
     * è¯¥å€¼ä¸ºã€Œè¿½æº¯ç½‘址」的唯一真值源:二维码内容与 H5 é¡µé¢å±•示的追溯网址均取自此配置。
     * å¿…须为公众扫码可访问的地址,非本地环境配置为内网/占位符地址时启动将直接失败。
     */
    private String traceBaseUrl = "";
    /**
     * ç”Ÿäº§ç»è¥è€…名称(种子标签四要素之一)。
     * <p>
     * ä½œä¸ºã€Œç”Ÿäº§ç»è¥è€…名称」的唯一真值源,标签印刷与 H5 æº¯æºé¡µå‡å–此值,禁止在前端硬编码。
     */
    private String operatorName = "";
    /**
     * è¿›å£å•†åç§°ï¼ˆè¿›å£ç§å­åœºæ™¯ï¼Œæ ‡ç­¾å››è¦ç´ ä¹‹ä¸€ï¼‰ã€‚
     * <p>
     * è¿›å£ç§å­åº”标注进口商名称;为空时回退使用 {@link #operatorName}。
     */
    private String importerName = "";
    /**
     * æ˜¯å¦å¤„于进口种子场景:为 true æ—¶æ ‡ç­¾ä¸Žæº¯æºé¡µå±•示「进口商名称」而非「生产经营者名称」
     */
    private boolean imported = false;
    /**
     * å…è®¸äºŒç»´ç ç¼–码纯码值(不带溯源 URL)。
     * <p>
     * ä»…供本地/测试环境兼容历史码使用;生产环境必须为 false,否则会印出不含追溯网址的标签。
     */
    private boolean allowPlainQrCode = false;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/config/MesQrCodeConfig.java
@@ -1,22 +1,142 @@
package cn.iocoder.yudao.module.mes.framework.config;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.framework.qrcode.MesProQrLabelBuilder;
import cn.iocoder.yudao.module.mes.framework.qrcode.QrCodeImageUtils;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import java.net.URI;
import java.util.Arrays;
/**
 * MES äºŒç»´ç ç›¸å…³é…ç½®ï¼šå¯åŠ¨æ—¶å°† traceBaseUrl æ³¨å…¥äºŒç»´ç æ ‡ç­¾æž„建器,避免侵入 Controller è°ƒç”¨ç‚¹
 * MES äºŒç»´ç ç›¸å…³é…ç½®ï¼šå¯åŠ¨æ—¶æ ¡éªŒå¹¶æŠŠé…ç½®æ³¨å…¥äºŒç»´ç æ ‡ç­¾æž„å»ºå™¨ï¼Œé¿å…ä¾µå…¥ Controller è°ƒç”¨ç‚¹ã€‚
 * <p>
 * ç§å­æ ‡ç­¾å±žäºŽå—国标约束的合规物料,一旦印出即无法召回,因此这里采取「启动即校验、不合法即拒绝启动」
 * çš„策略,而不是等到导出时才失败或静默降级:
 * <ul>
 *     <li>追溯网址必须为公众可访问地址,禁止内网 IP / æœ¬æœºåœ°å€ / æœªæ›¿æ¢çš„占位符;</li>
 *     <li>生产经营者名称必填(标签四要素之一,缺失即为不合规标签);</li>
 *     <li>当前环境必须存在可渲染中文的字体(否则标签中文会变成方框)。</li>
 * </ul>
 */
@Slf4j
@Configuration(proxyBeanMethods = false)
public class MesQrCodeConfig {
    /**
     * æœ¬åœ°çŽ¯å¢ƒ profile åï¼šè¯¥ profile ä¸‹æ”¾å®½æ ¡éªŒï¼Œä¾¿äºŽæœ¬åœ°å¼€å‘调试
     */
    private static final String PROFILE_LOCAL = "local";
    @Resource
    private MesProperties mesProperties;
    @Resource
    private Environment environment;
    @PostConstruct
    public void init() {
        // 1. æ³¨å…¥é…ç½®ï¼ˆå­—体校验独立于业务配置,本地环境同样必须通过,否则标签中文不可读)
        MesProQrLabelBuilder.setTraceBaseUrl(mesProperties.getTraceBaseUrl());
        MesProQrLabelBuilder.setOperatorName(mesProperties.getOperatorName());
        MesProQrLabelBuilder.setImporterName(mesProperties.getImporterName());
        MesProQrLabelBuilder.setImported(mesProperties.isImported());
        MesProQrLabelBuilder.setAllowPlainQrCode(mesProperties.isAllowPlainQrCode());
        QrCodeImageUtils.assertCjkFontAvailable();
        // 2. éžæœ¬åœ°çŽ¯å¢ƒæ‰§è¡Œä¸¥æ ¼æ ¡éªŒ
        if (isLocalProfile()) {
            log.info("[init] å½“前为本地环境({}),跳过标签合规配置的严格校验;traceBaseUrl={}",
                    PROFILE_LOCAL, mesProperties.getTraceBaseUrl());
            return;
        }
        validateTraceBaseUrl();
        validateOperatorName();
        log.info("[init] äºŒç»´ç æ ‡ç­¾é…ç½®æ ¡éªŒé€šè¿‡ï¼›traceBaseUrl={},生产经营者={}",
                mesProperties.getTraceBaseUrl(), MesProQrLabelBuilder.resolveOperatorName());
    }
    private boolean isLocalProfile() {
        return Arrays.asList(environment.getActiveProfiles()).contains(PROFILE_LOCAL);
    }
    /**
     * æ ¡éªŒè¿½æº¯ç½‘址:必须为公众扫码可访问的地址
     */
    private void validateTraceBaseUrl() {
        String url = StrUtil.trim(mesProperties.getTraceBaseUrl());
        if (StrUtil.isEmpty(url)) {
            throw new IllegalStateException("mes.trace-base-url æœªé…ç½®ã€‚二维码标签必须包含追溯网址,"
                    + "请在配置中填写公众扫码可访问的 H5 æº¯æºé¡µåœ°å€ã€‚");
        }
        if (StrUtil.containsAnyIgnoreCase(url, "your-domain", "example.com", "localhost")) {
            throw new IllegalStateException("mes.trace-base-url ä»ä¸ºå ä½ç¬¦æˆ–本机地址:" + url
                    + ",印出的标签消费者将无法访问,请填写真实的外网地址。");
        }
        String host = extractHost(url);
        if (StrUtil.isEmpty(host)) {
            throw new IllegalStateException("mes.trace-base-url æ ¼å¼éžæ³•,无法解析主机名:" + url);
        }
        if (isPrivateHost(host)) {
            throw new IllegalStateException("mes.trace-base-url æŒ‡å‘内网地址(" + host + "):" + url
                    + ",公众扫码无法访问,请填写外网可访问的地址。");
        }
    }
    /**
     * æ ¡éªŒç”Ÿäº§ç»è¥è€…名称:标签四要素之一,缺失即为不合规标签
     * <p>
     * æ­¤å¤„仅告警不阻断启动:生产经营者名称按主体区分,部分环境可能尚未配置,
     * å¼ºåˆ¶å¯åŠ¨å¤±è´¥ä¼šå½±å“æ— å…³åŠŸèƒ½ã€‚çœŸæ­£çš„æ‹¦æˆªç‚¹åœ¨æ ‡ç­¾æž„å»ºå¤„ï¼ˆç¼ºå¤±æ—¶æ‹’ç»å¯¼å‡ºï¼‰ï¼Œ
     * å› æ­¤ä¸ä¼šå‡ºçް"配置缺失却印出不合规标签"的情况。
     */
    private void validateOperatorName() {
        if (StrUtil.isBlank(MesProQrLabelBuilder.resolveOperatorName())) {
            log.warn("[init] mes.operator-name(生产经营者名称)未配置。该字段为种子标签四要素之一,"
                    + "缺失时将无法导出二维码标签,请尽快在配置中补充。");
        }
    }
    /**
     * ä»Ž URL ä¸­æå–主机名
     */
    private String extractHost(String url) {
        try {
            URI uri = URI.create(url);
            return uri.getHost();
        } catch (IllegalArgumentException ex) {
            return null;
        }
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºæœ¬æœº / å†…网 / é“¾è·¯æœ¬åœ°åœ°å€
     */
    private boolean isPrivateHost(String host) {
        if ("localhost".equalsIgnoreCase(host) || host.endsWith(".local")) {
            return true;
        }
        String[] parts = host.split("\\.");
        if (parts.length != 4) {
            return false; // åŸŸåå½¢å¼ï¼Œäº¤ç”± DNS è§£æžï¼Œä¸åœ¨æ­¤å¤„拦截
        }
        int a, b;
        try {
            a = Integer.parseInt(parts[0]);
            b = Integer.parseInt(parts[1]);
        } catch (NumberFormatException ex) {
            return false;
        }
        // 127.0.0.0/8 æœ¬æœºã€10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 ç§ç½‘、169.254.0.0/16 é“¾è·¯æœ¬åœ°
        return a == 127
                || a == 10
                || (a == 172 && b >= 16 && b <= 31)
                || (a == 192 && b == 168)
                || (a == 169 && b == 254);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/MesProQrLabelBuilder.java
@@ -2,22 +2,42 @@
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import java.nio.charset.StandardCharsets;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_LABEL_ITEM_NAME_MISSING;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_LABEL_OPERATOR_MISSING;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_LABEL_TRACE_URL_NOT_CONFIGURED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_LABEL_UNIT_CODE_INVALID;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_LABEL_UNIT_CODE_MISSING;
/**
 * MES äºŒç»´ç æ ‡ç­¾æž„建器:统一"二维码编码内容 + å›¾ç‰‡å åŠ å¯è¯»æ–‡æœ¬"的文本组装规则,
 * ä¿è¯æ‰¹æ¬¡ã€æ‰˜ç›˜ã€è¢‹ç ä¸‰ç§æ ‡ç­¾çš„关键信息保留一致。
 * MES äºŒç»´ç æ ‡ç­¾æž„建器:统一"二维码编码内容 + å›¾ç‰‡å åŠ å¯è¯»æ–‡æœ¬"的文本组装规则。
 * <p>
 * é…ç½®äº† {@code mes.trace-base-url} æ—¶ï¼ŒäºŒç»´ç å†…容为 H5 æº¯æºåœ°å€ï¼ˆå¸¦ code å‚数),否则回退为纯码值(兼容旧码)
 * ç§å­æ ‡ç­¾å—《农作物种子标签二维码编码规则》与《农作物种子标签通则》(GB 20464-2024)约束,
 * æœ€å°é”€å”®å•元的标签必须包含四项信息且按固定顺序排列:
 * <ol>
 *     <li>品种名称</li>
 *     <li>生产经营者名称(进口种子为进口商名称)</li>
 *     <li>单元识别代码(不超过 20 ä¸ªå­—符,数字或字母数字组合)</li>
 *     <li>追溯网址</li>
 * </ol>
 * å››è¦ç´ ç¼ºå¤±å³æž„成不合规标签,因此这里采取「缺失即抛异常、拒绝导出」的策略,绝不静默跳过——
 * é™é»˜è·³è¿‡æ­£æ˜¯æ ‡ç­¾ç¼ºé¡¹æµå…¥å°åˆ·çŽ¯èŠ‚çš„æ ¹å› ã€‚
 * <p>
 * äºŒç»´ç å†…容在配置了 {@code mes.trace-base-url} æ—¶ä¸ºå¸¦ code å‚数的溯源 URL,否则按
 * {@code mes.allow-plain-qr-code} å†³å®šæ˜¯å¦é™çº§ä¸ºçº¯ç å€¼ï¼ˆä»…限本地兼容历史码)。
 */
public final class MesProQrLabelBuilder {
@@ -25,20 +45,78 @@
    }
    /**
     * H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼Œç”± {@code MesQrCodeConfig} å¯åŠ¨æ—¶æ³¨å…¥ï¼›ä¸ºç©ºæ—¶äºŒç»´ç ç¼–ç çº¯ç å€¼
     * å•元识别代码格式:1~20 ä½æ•°å­—或字母数字组合(国标要求,不含连字符等符号)
     */
    private static final Pattern UNIT_CODE_PATTERN = Pattern.compile("^[0-9A-Za-z]{1,20}$");
    /** H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼Œç”± {@code MesQrCodeConfig} å¯åŠ¨æ—¶æ³¨å…¥ï¼›ä¸ºç©ºæ—¶äºŒç»´ç ç¼–ç çº¯ç å€¼ */
    private static volatile String traceBaseUrl = "";
    /** ç”Ÿäº§ç»è¥è€…名称,由 {@code MesQrCodeConfig} å¯åŠ¨æ—¶æ³¨å…¥ */
    private static volatile String operatorName = "";
    /** è¿›å£å•†åç§°ï¼Œè¿›å£ç§å­åœºæ™¯ä½¿ç”¨ */
    private static volatile String importerName = "";
    /** æ˜¯å¦è¿›å£ç§å­åœºæ™¯ */
    private static volatile boolean imported = false;
    /** æ˜¯å¦å…è®¸äºŒç»´ç é™çº§ç¼–码纯码值(仅限本地兼容历史码) */
    private static volatile boolean allowPlainQrCode = false;
    public static void setTraceBaseUrl(String traceBaseUrl) {
        MesProQrLabelBuilder.traceBaseUrl = StrUtil.blankToDefault(traceBaseUrl, "");
    }
    public static void setOperatorName(String operatorName) {
        MesProQrLabelBuilder.operatorName = StrUtil.blankToDefault(operatorName, "");
    }
    public static void setImporterName(String importerName) {
        MesProQrLabelBuilder.importerName = StrUtil.blankToDefault(importerName, "");
    }
    public static void setImported(boolean imported) {
        MesProQrLabelBuilder.imported = imported;
    }
    public static void setAllowPlainQrCode(boolean allowPlainQrCode) {
        MesProQrLabelBuilder.allowPlainQrCode = allowPlainQrCode;
    }
    /**
     * ç»„装二维码内容:配置了溯源地址则拼 URL,否则返回纯码值
     * å½“前应标注的生产经营者名称:进口种子场景优先取进口商名称,否则取生产经营者名称
     *
     * @return åç§°ï¼Œæœªé…ç½®æ—¶è¿”回空串
     */
    public static String resolveOperatorName() {
        if (imported && StrUtil.isNotBlank(importerName)) {
            return importerName;
        }
        return operatorName;
    }
    /**
     * è¿½æº¯ç½‘址(对外展示用,不含 code å‚数)
     */
    public static String resolveTraceUrl() {
        return traceBaseUrl;
    }
    /**
     * æ˜¯å¦è¿›å£ç§å­åœºæ™¯ï¼šä¸º true æ—¶æ ‡ç­¾ä¸Žæº¯æºé¡µåº”展示「进口商名称」
     */
    public static boolean isImported() {
        return imported;
    }
    /**
     * ç»„装二维码内容:配置了溯源地址则拼 URL,否则按开关决定是否降级为纯码值
     * <p>
     * æœªé…ç½®æº¯æºåœ°å€ä¸”未开启降级开关时直接报错,避免印出不含追溯网址的标签。
     */
    private static String buildQrCodeContent(String code) {
        if (StrUtil.isEmpty(traceBaseUrl)) {
            return code;
            if (allowPlainQrCode) {
                return code;
            }
            throw exception(PRO_QR_LABEL_TRACE_URL_NOT_CONFIGURED);
        }
        return traceBaseUrl + "?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8);
    }
@@ -99,44 +177,115 @@
            keyValues.put("已装袋数", String.valueOf(pallet.getBagCount()));
        }
        if (batch != null) {
            // å“ç§å•独取批次品种:fillBatchKeyValues å·²ä¸å†å†™å…¥å“ç§ï¼Œé¿å…ä¸Žè¢‹ç å“ç§äº’相覆盖
            MesMdmItemDO batchItem = itemMap.get(batch.getItemId());
            if (batchItem != null && StrUtil.isNotEmpty(batchItem.getName())) {
                keyValues.put("品种", batchItem.getName());
            }
            fillBatchKeyValues(keyValues, batch, itemMap, userMap);
        }
        return QrLabelInfo.builder().fileName(code).code(buildQrCodeContent(code)).keyValues(keyValues).build();
    }
    /**
     * è¢‹ç äºŒç»´ç æ ‡ç­¾
     * è¢‹ç äºŒç»´ç æ ‡ç­¾ï¼ˆæœ€å°é”€å”®å•元)
     * <p>
     * æŒ‰å›½æ ‡è¦æ±‚,四要素置于标签最前并按固定顺序排列:品种名称 â†’ ç”Ÿäº§ç»è¥è€… â†’ å•元识别代码 â†’ è¿½æº¯ç½‘址;
     * è¢‹åºå·ã€æ‰¹æ¬¡å·ã€äº§çº¿ã€è´£ä»»äººç­‰ç”Ÿäº§ä¿¡æ¯ä½œä¸ºè¾…助内容排列在其后。四要素任一缺失即抛异常,拒绝导出。
     */
    public static QrLabelInfo buildBagCodeLabel(MesProBagCodeDO bagCode,
                                                MesProBatchDO batch,
                                                Map<Long, MesMdmItemDO> itemMap,
                                                Map<Long, AdminUserRespDTO> userMap) {
        LinkedHashMap<String, String> keyValues = new LinkedHashMap<>();
        keyValues.put("袋码", bagCode.getCode());
        keyValues.put("袋序号", bagCode.getBagNo() == null ? "" : String.valueOf(bagCode.getBagNo()));
        keyValues.put("批次号", bagCode.getBatchCode());
        // ===== å››è¦ç´ ï¼ˆå›½æ ‡è¦æ±‚,顺序固定,缺失即阻断导出)=====
        putRequired(keyValues, "品种名称", resolveItemName(bagCode, batch, itemMap),
                PRO_QR_LABEL_ITEM_NAME_MISSING);
        putRequired(keyValues, imported ? "进口商名称" : "生产经营者名称", resolveOperatorName(),
                PRO_QR_LABEL_OPERATOR_MISSING);
        putRequired(keyValues, "单元识别代码", resolveUnitCode(bagCode.getCode()),
                PRO_QR_LABEL_UNIT_CODE_MISSING);
        putRequired(keyValues, "追溯网址", resolveTraceUrl(), PRO_QR_LABEL_TRACE_URL_NOT_CONFIGURED);
        // ===== ä»¥ä¸‹ä¸ºè¾…助生产信息 =====
        if (bagCode.getBagNo() != null) {
            keyValues.put("袋序号", String.valueOf(bagCode.getBagNo()));
        }
        if (StrUtil.isNotEmpty(bagCode.getBatchCode())) {
            keyValues.put("批次号", bagCode.getBatchCode());
        }
        if (StrUtil.isNotEmpty(bagCode.getPalletCode())) {
            keyValues.put("绑定托盘", bagCode.getPalletCode());
        }
        if (batch != null) {
            fillBatchKeyValues(keyValues, batch, itemMap, userMap);
        }
        return QrLabelInfo.builder().fileName(bagCode.getCode()).code(buildQrCodeContent(bagCode.getCode())).keyValues(keyValues).build();
        return QrLabelInfo.builder()
                .fileName(bagCode.getCode())
                .code(buildQrCodeContent(bagCode.getCode()))
                .keyValues(keyValues)
                .build();
    }
    /**
     * å¡«å……批次公共可读文本(品种、规格、产线、生产日期、责任人)
     * å†™å…¥å¿…填字段:值为空时抛出对应异常,绝不静默跳过
     */
    private static void putRequired(LinkedHashMap<String, String> keyValues, String label,
                                    String value, ErrorCode errorCode) {
        if (StrUtil.isBlank(value)) {
            throw exception(errorCode);
        }
        keyValues.put(label, value.trim());
    }
    /**
     * è§£æžå“ç§åç§°ï¼šä¼˜å…ˆå–袋码自身绑定的品种(数据包生成时写入,未录批次也可印刷),
     * è¢‹ç æœªç»‘定品种时回退批次品种。
     */
    private static String resolveItemName(MesProBagCodeDO bagCode, MesProBatchDO batch,
                                          Map<Long, MesMdmItemDO> itemMap) {
        if (StrUtil.isNotBlank(bagCode.getItemName())) {
            return bagCode.getItemName();
        }
        if (batch == null) {
            return null;
        }
        MesMdmItemDO item = itemMap.get(batch.getItemId());
        return item == null ? null : item.getName();
    }
    /**
     * æ ¡éªŒå¹¶è¿”回单元识别代码(国标:不超过 20 ä¸ªå­—符,数字或字母数字组合)
     */
    private static String resolveUnitCode(String code) {
        if (StrUtil.isBlank(code)) {
            return null;
        }
        if (!UNIT_CODE_PATTERN.matcher(code).matches()) {
            throw exception(PRO_QR_LABEL_UNIT_CODE_INVALID);
        }
        return code;
    }
    /**
     * å¡«å……批次公共可读文本(规格、产线、生产日期、责任人)
     * <p>
     * æ³¨æ„ï¼šæ­¤å¤„不再写入「品种」,品种名称统一由 {@link #resolveItemName} è§£æžï¼Œ
     * é¿å…æ‰¹æ¬¡å“ç§è¦†ç›–袋码品种导致标签与溯源页展示不一致。
     */
    private static void fillBatchKeyValues(LinkedHashMap<String, String> keyValues, MesProBatchDO batch,
                                           Map<Long, MesMdmItemDO> itemMap, Map<Long, AdminUserRespDTO> userMap) {
        MesMdmItemDO item = itemMap.get(batch.getItemId());
        if (item != null) {
            keyValues.put("品种", item.getName());
        if (item != null && StrUtil.isNotEmpty(item.getSpecification())) {
            keyValues.put("规格", item.getSpecification());
        }
        keyValues.put("产线", StrUtil.join(" / ", batch.getLineName(), batch.getLineCode()));
        keyValues.put("生产日期", batch.getProduceDate() == null ? ""
                : LocalDateTimeUtil.format(batch.getProduceDate(), "yyyy-MM-dd HH:mm"));
        if (StrUtil.isNotEmpty(batch.getLineName()) || StrUtil.isNotEmpty(batch.getLineCode())) {
            keyValues.put("产线", StrUtil.join(" / ", batch.getLineName(), batch.getLineCode()));
        }
        if (batch.getProduceDate() != null) {
            keyValues.put("生产日期", LocalDateTimeUtil.format(batch.getProduceDate(), "yyyy-MM-dd HH:mm"));
        }
        AdminUserRespDTO user = userMap.get(batch.getOwnerUserId());
        if (user != null) {
            keyValues.put("责任人", user.getNickname());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/framework/qrcode/QrCodeImageUtils.java
@@ -28,7 +28,8 @@
/**
 * äºŒç»´ç æ ‡ç­¾å›¾ç‰‡å·¥å…·ç±»
 * <p>
 * ç”Ÿæˆ"二维码 + å³ä¾§å¯è¯»æ–‡æœ¬"的合成图片:二维码只编码纯码值,文本叠加关键信息,
 * ç”Ÿæˆ"二维码 + å³ä¾§å¯è¯»æ–‡æœ¬"的合成图片:二维码编码内容由调用方传入(配置了溯源地址时为
 * å¸¦ code å‚数的溯源 URL,未配置时为纯码值),右侧叠加关键信息文本,
 * å³ä½¿æ•°æ®åº“丢失也能从打印图片还原关键信息。
 */
@Slf4j
@@ -55,11 +56,21 @@
    /** å­—段间距 */
    private static final int FIELD_GAP = 6;
    /** ä¸­æ–‡å­—体候选(按优先级),避免 Linux headless ä¸‹æ–‡å­—变方框 */
    /**
     * ä¸­æ–‡å­—体候选(按优先级)。
     * <p>
     * åªå…è®¸æ”¶å½•可渲染中文的字体:纯拉丁字体(如 Arial)一旦入选,中文会全部渲染成方框,
     * ä¸”导出过程不报错,属于最隐蔽的一类事故。
     */
    private static final String[] FONT_CANDIDATES = {
            "Noto Sans CJK SC", "WenQuanYi Zen Hei", "Microsoft YaHei", "SimSun", "Arial"
            "Noto Sans CJK SC", "Source Han Sans SC", "WenQuanYi Zen Hei", "PingFang SC",
            "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "SimSun"
    };
    /** ä¸­æ–‡å­—形探针字符(用于探测字体是否具备中文字形) */
    private static final char CJK_PROBE = '袋';
    /** è§£æžå‡ºçš„中文字体族名;为 null è¡¨ç¤ºå½“前环境没有任何可渲染中文的字体 */
    private static final String FONT_NAME = resolveFontName();
    /**
@@ -70,6 +81,8 @@
     * @return åˆæˆå›¾ç‰‡
     */
    public static BufferedImage createQrLabel(String code, LinkedHashMap<String, String> keyValues) {
        // æ— ä¸­æ–‡å­—体时直接失败,避免导出中文为方框的标签
        assertCjkFontAvailable();
        QrConfig config = new QrConfig();
        config.setErrorCorrection(ErrorCorrectionLevel.H); // æ‰“印标签易磨损,用高容错
        config.setMargin(1); // ä¿è¯é™åŒºï¼Œæ‰«ç çއ
@@ -134,14 +147,46 @@
    // ==================== ç§æœ‰æ–¹æ³• ====================
    /**
     * è§£æžå½“前环境可用的中文字体族名。
     * <p>
     * ä¸èƒ½æŒ‰å­—体族名做精确匹配:同一字体在不同系统上的族名可能不同(例如 Windows ä¸Šå®žé™…注册为
     * {@code Microsoft YaHei UI} è€Œéž {@code Microsoft YaHei}),精确匹配落空后会一路退到纯拉丁字体,
     * ä¸­æ–‡å³æ¸²æŸ“为方框。因此这里统一用 {@link Font#canDisplay(char)} æŽ¢æµ‹ä¸­æ–‡å­—形。
     *
     * @return å¯æ¸²æŸ“中文的字体族名;无可用字体时返回 {@code null}
     */
    private static String resolveFontName() {
        Set<String> families = Set.of(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
        for (String candidate : FONT_CANDIDATES) {
            if (families.contains(candidate)) {
            if (families.contains(candidate) && canDisplayCjk(candidate)) {
                return candidate;
            }
        }
        return Font.SANS_SERIF;
        // é€»è¾‘字体由 JDK è´Ÿè´£å­—形回退,多数环境可渲染中文
        if (canDisplayCjk(Font.SANS_SERIF)) {
            return Font.SANS_SERIF;
        }
        return null;
    }
    /**
     * æŽ¢æµ‹æŒ‡å®šå­—体族能否渲染中文
     */
    private static boolean canDisplayCjk(String family) {
        return new Font(family, Font.PLAIN, FONT_SIZE).canDisplay(CJK_PROBE);
    }
    /**
     * æ ¡éªŒå½“前环境存在可渲染中文的字体,否则抛出异常。
     * <p>
     * ä¾›å¯åŠ¨æœŸè°ƒç”¨ï¼šå®å¯åœ¨å¯åŠ¨æ—¶å¤±è´¥ï¼Œä¹Ÿä¸èƒ½æŠŠä¸­æ–‡æ¸²æŸ“æˆæ–¹æ¡†çš„æ ‡ç­¾äº¤ä»˜å°åˆ·ã€‚
     */
    public static void assertCjkFontAvailable() {
        if (FONT_NAME == null) {
            throw new IllegalStateException("当前环境没有任何可渲染中文的字体,二维码标签的中文将显示为方框,"
                    + "拒绝生成标签。请在服务器安装中文字体(如 fonts-noto-cjk)后重启。");
        }
    }
    private static int measureTextHeight(LinkedHashMap<String, String> keyValues, Font labelFont, Font valueFont) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/home/MesBagCodeHomeStatisticsServiceImpl.java
@@ -42,7 +42,16 @@
        summary.setBagCodeCount(getCount(homeStatisticsMapper.selectBagCodeCount()));
        summary.setPalletCount(getCount(homeStatisticsMapper.selectPalletCount()));
        // 3. çŠ¶æ€åˆ†å¸ƒ
        // 3. å‡ºåº“流通
        summary.setSalesNoticeCount(getCount(homeStatisticsMapper.selectSalesNoticeCount()));
        summary.setProductSalesCount(getCount(homeStatisticsMapper.selectProductSalesCount()));
        summary.setReturnSalesCount(getCount(homeStatisticsMapper.selectReturnSalesCount()));
        // 4. ä»“储预警 / è´¨é‡
        summary.setStockWarningPendingCount(getCount(homeStatisticsMapper.selectStockWarningPendingCount()));
        summary.setNcrPendingCount(getCount(homeStatisticsMapper.selectNcrPendingCount()));
        // 5. çŠ¶æ€åˆ†å¸ƒ
        summary.setBatchStatusList(toStatusList(homeStatisticsMapper.selectBatchCountGroupByStatus()));
        summary.setBagCodeStatusList(toStatusList(homeStatisticsMapper.selectBagCodeCountGroupByStatus()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodeRecordServiceImpl.java
@@ -4,6 +4,7 @@
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO;
@@ -13,6 +14,7 @@
import cn.iocoder.yudao.module.mes.service.md.autocode.strategy.MesMdAutoCodePartStrategy;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@@ -41,6 +43,9 @@
    private MesMdAutoCodeRuleService ruleService;
    @Resource
    private MesMdAutoCodePartService partService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private List<MesMdAutoCodePartStrategy> strategyList;
@@ -93,23 +98,17 @@
     */
    private String generateCode(MesMdAutoCodeRuleDO rule, List<MesMdAutoCodePartDO> parts,
                                MesMdAutoCodeContext context) {
        StringBuilder codeBuilder = new StringBuilder();
        for (MesMdAutoCodePartDO part : parts) {
            MesMdAutoCodePartStrategy strategy = strategyMap.get(part.getType());
            Assert.notNull(strategy, () -> exception(AUTO_CODE_GENERATE_FAILED));
            // ç”Ÿæˆåˆ†æ®µç¼–码
            String partCode = strategy.generate(part, context);
            // ç»Ÿä¸€æˆªå–指定长度
            partCode = StrUtil.sub(partCode, 0, part.getLength());
            // æ‹¼æŽ¥åˆ†æ®µç¼–码
            codeBuilder.append(partCode);
        // éœ€æ±‚:所有业务编码统一为「16 ä½çº¯æ•°å­—」,且全系统唯一。
        // å› æ­¤ä¸å†æŒ‰è§„则分段拼装(固定字符/日期段会带入字母),改为取全系统统一自增序列。
        // éšæœºç”Ÿæˆ 16 ä½çº¯æ•°å­—编码,并用 Redis é›†åˆæŸ¥é‡ï¼Œä¿è¯å…¨ç³»ç»Ÿå”¯ä¸€ï¼ˆä¸ä½¿ç”¨è‡ªå¢ž/顺序)
        for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
            String code = BizNoUtils.randomCode();
            Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
            if (added != null && added > 0) {
                return code;
            }
        }
        // è¡¥é½å¤„理
        String result = codeBuilder.toString();
        if (Boolean.TRUE.equals(rule.getPadded()) && rule.getMaxLength() != null) {
            result = padCode(result, rule);
        }
        return result;
        throw new IllegalStateException("生成唯一业务编码失败,请重试");
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -137,8 +137,10 @@
        validateItemCodeUnique(reqVO.getId(), reqVO.getCode());
        // æ ¡éªŒç‰©æ–™åç§°çš„唯一性
        validateItemNameUnique(reqVO.getId(), reqVO.getName());
        // æ ¡éªŒç‰©æ–™åˆ†ç±»å­˜åœ¨
        validateItemTypeExists(reqVO.getItemTypeId());
        // æ ¡éªŒç‰©æ–™åˆ†ç±»å­˜åœ¨ï¼ˆMDM å“ç§åŒæ­¥ä¸ä¼ åˆ†ç±»ï¼Œå…è®¸ä¸ºç©ºï¼‰
        if (reqVO.getItemTypeId() != null) {
            validateItemTypeExists(reqVO.getItemTypeId());
        }
        // æ ¡éªŒè®¡é‡å•位存在
        validateUnitMeasureExists(reqVO.getUnitMeasureId());
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeService.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeRawGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
@@ -30,6 +31,15 @@
     * @return ç”Ÿæˆç»„ UUID(标记同一次生成的数据包)
     */
    MesProBagCodeGenerateRespVO generateBagCodes(@Valid MesProBagCodeGenerateReqVO generateReqVO);
    /**
     * ç”ŸæˆåŽŸå§‹è¢‹ç ï¼šä»…é€‰å“ç§+袋码数量,不校验计量单位类型、不生成托盘;
     * è¢‹ç å¸¦å“ç§å†—余,后续可正常录入批次/绑定托盘
     *
     * @param rawGenerateReqVO ç”Ÿæˆä¿¡æ¯
     * @return ç”Ÿæˆç»„ UUID ä¸Žæ•°é‡ï¼ˆtype/perPallet/palletIds ä¸ºç©ºï¼‰
     */
    MesProBagCodeGenerateRespVO generateRawBagCodes(@Valid MesProBagCodeRawGenerateReqVO rawGenerateReqVO);
    void bindBatch(@Valid MesProBagCodeBindBatchReqVO bindReqVO);
@@ -130,6 +140,25 @@
    List<MesProBagCodeDO> getBagCodeListByBatchId(Long batchId);
    /**
     * èŽ·å¾—æŸæ‰¹æ¬¡ä¸‹å¯å°åˆ·çš„è¢‹ç åˆ—è¡¨ï¼ˆæŽ’é™¤å·²ä½œåºŸè¢‹ç ï¼ŒæŒ‰è¢‹åºå·å‡åºï¼‰ï¼Œç”¨äºŽæ‰¹é‡å¯¼å‡ºäºŒç»´ç æ ‡ç­¾
     * <p>
     * å·²ä½œåºŸçš„袋码(破损替换后作废)不得再印刷:其标签若贴到产品上,消费者扫码会被提示
     * ã€Œè¯¥è¿½æº¯ç å·²ä½œåºŸï¼Œè°¨é˜²å‡å†’」,导致真品被误判为假货。
     *
     * @param batchId æ‰¹æ¬¡ç¼–号
     * @return å¯å°åˆ·çš„袋码列表
     */
    List<MesProBagCodeDO> getPrintableBagCodeListByBatchId(Long batchId);
    /**
     * èŽ·å¾—æŸç”Ÿæˆç»„ä¸‹å¯å°åˆ·çš„è¢‹ç åˆ—è¡¨ï¼ˆæŽ’é™¤å·²ä½œåºŸè¢‹ç ï¼‰ï¼Œç”¨äºŽæ‰¹é‡å¯¼å‡ºäºŒç»´ç æ ‡ç­¾
     *
     * @param uuid ç”Ÿæˆç»„ UUID
     * @return å¯å°åˆ·çš„袋码列表
     */
    List<MesProBagCodeDO> getPrintableBagCodeListByUuid(String uuid);
    /**
     * æ ‡è®°è¢‹ç ä¸ºå·²å‡ºåº“(记录销售出库单编号和出库时间)
     *
     * @param id      è¢‹ç ç¼–号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/bagcode/MesProBagCodeServiceImpl.java
@@ -3,15 +3,20 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeBindBatchReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeBindReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeGenerateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeRawGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceItemRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeReplaceRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeReplaceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
@@ -20,9 +25,11 @@
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeReplaceMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -34,8 +41,13 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -61,29 +73,145 @@
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MdmUnitMeasureApi unitMeasureApi;
    /**
     * è®¡é‡å•位类型「吨」(无托盘模式)
     */
    private static final String UNIT_TYPE_TON = "吨";
    /**
     * è®¡é‡å•位类型「N千克」:每托盘装 N ä¸ªè¢‹ç 
     */
    private static final Pattern KG_TYPE_PATTERN = Pattern.compile("^(\\d{1,5})千克$");
    @Resource
    private MesCodeRedisDAO codeRedisDao;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProBagCodeGenerateRespVO generateBagCodes(MesProBagCodeGenerateReqVO generateReqVO) {
        if (generateReqVO.getCount() == null || generateReqVO.getCount() <= 0) {
            throw exception(PRO_BAG_CODE_GENERATE_COUNT_INVALID);
        // 1. æ ¡éªŒå“ç§å¹¶è¯»å–主计量单位类型
        MesMdmItemDO item = mdmItemService.validateItemExists(generateReqVO.getItemId());
        String type = getUnitType(item);
        // 2. æŒ‰ç±»åž‹æ¢ç®—袋码数量:N千克 = æ¯æ‰˜ N è¢‹ Ã— æ‰˜ç›˜æ•°ï¼›å¨ = ç›´æŽ¥è¾“入袋码数(无托盘)
        boolean ton = UNIT_TYPE_TON.equals(type);
        int perPallet = ton ? 0 : parseKgPerPallet(type);
        int totalCount;
        int palletCount = 0;
        if (ton) {
            if (generateReqVO.getBagCount() == null || generateReqVO.getBagCount() <= 0) {
                throw exception(PRO_BAG_CODE_GENERATE_COUNT_INVALID);
            }
            totalCount = generateReqVO.getBagCount();
        } else {
            if (generateReqVO.getPalletCount() == null || generateReqVO.getPalletCount() <= 0) {
                throw exception(PRO_BAG_CODE_PALLET_COUNT_REQUIRED);
            }
            palletCount = generateReqVO.getPalletCount();
            totalCount = perPallet * palletCount;
        }
        // ç”Ÿæˆé˜¶æ®µåªåˆ›å»ºå…¨å±€å”¯ä¸€è¢‹ç ï¼Œç”Ÿäº§æ‰¹æ¬¡åœ¨åŽç»­å½•å…¥
        int startNo = bagCodeMapper.selectMaxBagNo() + 1;
        // 3. ç”Ÿæˆé˜¶æ®µï¼šè¢‹ç ç›´æŽ¥ç»‘定品种;生产批次在后续录入
        String uuid = IdUtil.fastSimpleUUID();
        List<MesProPalletDO> pallets = new ArrayList<>(palletCount);
        if (!ton) {
            for (int i = 0; i < palletCount; i++) {
                pallets.add(new MesProPalletDO()
                        .setTempCode(codeRedisDao.generate())
                        .setBagCount(perPallet)
                        .setAutoGenerated(true)
                        .setStatus(MesProPalletStatusEnum.IN_USE.getStatus()));
            }
            palletMapper.insertBatch(pallets);
        }
        // è¢‹åºå·åŽŸå­åˆ†é…ï¼šå…ˆå–åº“å†…æœ€å¤§åºå·ä½œåŸºå‡†ï¼Œå†ç”± Redis åŽŸå­è‡ªå¢žï¼Œé¿å…å¹¶å‘ç”Ÿæˆäº§ç”Ÿé‡å¤è¢‹åºå·
        int startNo = codeRedisDao.allocateBagNoStart(totalCount, bagCodeMapper.selectMaxBagNo());
        List<MesProBagCodeDO> bagCodes = new ArrayList<>(totalCount);
        for (int i = 0; i < totalCount; i++) {
            int bagNo = startNo + i;
            MesProPalletDO pallet = ton ? null : pallets.get(i / perPallet);
            bagCodes.add(new MesProBagCodeDO()
                    .setUuid(uuid)
                    .setCode(codeRedisDao.generate())
                    .setBagNo(bagNo)
                    .setItemId(item.getId())
                    .setItemCode(item.getCode())
                    .setItemName(item.getName())
                    .setPalletId(pallet == null ? null : pallet.getId())
                    .setPalletCode(pallet == null ? null : pallet.getTempCode())
                    .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus())
                    .setRemark(generateReqVO.getRemark()));
        }
        bagCodeMapper.insertBatch(bagCodes);
        return new MesProBagCodeGenerateRespVO()
                .setUuid(uuid)
                .setType(type)
                .setPerPallet(perPallet)
                .setTotalCount(totalCount)
                .setPalletIds(pallets.stream().map(MesProPalletDO::getId).toList());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProBagCodeGenerateRespVO generateRawBagCodes(MesProBagCodeRawGenerateReqVO generateReqVO) {
        // åŽŸå§‹è¢‹ç ä¸ä¾èµ–è®¡é‡å•ä½ç±»åž‹ä¸Žæ‰˜ç›˜ï¼šä»…æŒ‰æ•°é‡ç”Ÿæˆï¼Œè¢‹ç å¸¦å“ç§å†—ä½™ï¼ŒåŽç»­å¯æ­£å¸¸å½•å…¥æ‰¹æ¬¡/绑定托盘
        MesMdmItemDO item = mdmItemService.validateItemExists(generateReqVO.getItemId());
        String uuid = IdUtil.fastSimpleUUID();
        // è¢‹åºå·åŽŸå­åˆ†é…ï¼šå…ˆå–åº“å†…æœ€å¤§åºå·ä½œåŸºå‡†ï¼Œå†ç”± Redis åŽŸå­è‡ªå¢žï¼Œé¿å…å¹¶å‘ç”Ÿæˆäº§ç”Ÿé‡å¤è¢‹åºå·
        int startNo = codeRedisDao.allocateBagNoStart(generateReqVO.getCount(), bagCodeMapper.selectMaxBagNo());
        List<MesProBagCodeDO> bagCodes = new ArrayList<>(generateReqVO.getCount());
        for (int i = 0; i < generateReqVO.getCount(); i++) {
            int bagNo = startNo + i;
            bagCodes.add(new MesProBagCodeDO()
                    .setUuid(uuid)
                    .setCode("BAG-" + StrUtil.padPre(String.valueOf(bagNo), 8, '0'))
                    .setCode(codeRedisDao.generate())
                    .setBagNo(bagNo)
                    .setItemId(item.getId())
                    .setItemCode(item.getCode())
                    .setItemName(item.getName())
                    .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus())
                    .setRemark(generateReqVO.getRemark()));
        }
        bagCodeMapper.insertBatch(bagCodes);
        return new MesProBagCodeGenerateRespVO().setUuid(uuid);
        return new MesProBagCodeGenerateRespVO()
                .setUuid(uuid)
                .setTotalCount(bagCodes.size())
                .setPalletIds(List.of());
    }
    /**
     * è¯»å–品种主计量单位的类型(48千克/100千克/吨),未配置或无法识别时报错
     */
    private String getUnitType(MesMdmItemDO item) {
        String type = null;
        if (item.getUnitMeasureId() != null) {
            CommonResult<MdmUnitMeasureRespDTO> result = unitMeasureApi.getUnitMeasure(item.getUnitMeasureId());
            if (result != null && result.isSuccess() && result.getData() != null) {
                type = StrUtil.trimToNull(result.getData().getType());
            }
        }
        if (type == null) {
            throw exception(PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED);
        }
        if (!UNIT_TYPE_TON.equals(type) && !KG_TYPE_PATTERN.matcher(type).matches()) {
            throw exception(PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED);
        }
        return type;
    }
    /**
     * è§£æžã€ŒN千克」类型得出每托盘袋数
     */
    private int parseKgPerPallet(String type) {
        Matcher matcher = KG_TYPE_PATTERN.matcher(type);
        if (!matcher.matches()) {
            throw exception(PRO_BAG_CODE_ITEM_TYPE_NOT_CONFIGURED);
        }
        return Integer.parseInt(matcher.group(1));
    }
    @Override
@@ -103,15 +231,60 @@
        List<Long> ids = bindReqVO.getIds().stream().distinct().toList();
        List<MesProBagCodeDO> bagCodes = bagCodeMapper.selectListByIdsForUpdate(ids);
        if (bagCodes.size() != ids.size() || bagCodes.stream().anyMatch(code ->
                code.getBatchId() != null || !MesProBagCodeStatusEnum.GENERATED.getStatus().equals(code.getStatus()))) {
                code.getBatchId() != null
                        || (!MesProBagCodeStatusEnum.GENERATED.getStatus().equals(code.getStatus())
                        && !MesProBagCodeStatusEnum.PRINTED.getStatus().equals(code.getStatus())))) {
            throw exception(PRO_BAG_CODE_STATUS_NOT_GENERATED);
        }
        // å½•入的袋码必须属于同一品种,且与批次已有袋码品种一致(历史空白袋码 item ä¸ºç©ºä¸å‚与校验)
        Set<Long> itemIds = new LinkedHashSet<>();
        bagCodes.stream().map(MesProBagCodeDO::getItemId).filter(Objects::nonNull).forEach(itemIds::add);
        itemIds.addAll(bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId()));
        if (itemIds.size() > 1) {
            throw exception(PRO_BAG_CODE_BIND_BATCH_ITEM_MISMATCH);
        }
        if (bagCodeMapper.bindBatchByIds(ids, batch.getId(), batch.getCode(),
                MesProBagCodeStatusEnum.GENERATED.getStatus()) != ids.size()) {
                List.of(MesProBagCodeStatusEnum.GENERATED.getStatus(),
                        MesProBagCodeStatusEnum.PRINTED.getStatus())) != ids.size()) {
            throw exception(PRO_BAG_CODE_STATUS_NOT_GENERATED);
        }
        batchMapper.updateById(new MesProBatchDO().setId(batch.getId())
                .setBagQuantity(bagCodeMapper.selectEffectiveCountByBatchId(batch.getId())));
        // çº§è”:本组袋码绑定的未入批次托盘,分配正式托盘码并绑定批次
        bindPalletsToBatch(bagCodes, batch);
    }
    /**
     * è¢‹ç å½•入批次时,将其绑定的临时托盘(未绑定批次)级联绑定到同一批次:
     * åˆ†é…æ­£å¼æ‰˜ç›˜ç ï¼ˆ16 ä½éšæœºæ•°å­—,与托盘码生成规则一致)、回写袋码上的冗余托盘码、累计批次托盘数
     */
    private void bindPalletsToBatch(List<MesProBagCodeDO> bagCodes, MesProBatchDO batch) {
        List<Long> palletIds = bagCodes.stream().map(MesProBagCodeDO::getPalletId)
                .filter(Objects::nonNull).distinct().sorted().toList();
        if (palletIds.isEmpty()) {
            return;
        }
        int nextPalletNo = palletMapper.selectMaxPalletNo(batch.getId()) + 1;
        int boundCount = 0;
        for (Long palletId : palletIds) {
            MesProPalletDO pallet = palletMapper.selectByIdForUpdate(palletId);
            if (pallet == null || pallet.getBatchId() != null || StrUtil.isNotEmpty(pallet.getCode())) {
                continue;
            }
            String palletCode = codeRedisDao.generate();
            palletMapper.updateById(new MesProPalletDO().setId(palletId)
                    .setCode(palletCode)
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setPalletNo(nextPalletNo++));
            // è¢‹ç ä¸Šçš„冗余托盘码由临时码更新为正式码
            bagCodeMapper.updatePalletCodeByPalletId(palletId, palletCode);
            boundCount++;
        }
        if (boundCount > 0) {
            batchMapper.updateById(new MesProBatchDO().setId(batch.getId())
                    .setPalletQuantity((batch.getPalletQuantity() == null ? 0 : batch.getPalletQuantity()) + boundCount));
        }
    }
    @Override
@@ -171,10 +344,13 @@
            MesProPalletDO pallet = oldBagCode.getPalletId() == null ? null : palletMap.get(oldBagCode.getPalletId());
            MesProBagCodeDO newBagCode = new MesProBagCodeDO()
                    .setUuid(replaceUuid)
                    .setCode(batch.getCode() + "-" + StrUtil.padPre(String.valueOf(newBagNo), 4, '0'))
                    .setCode(codeRedisDao.generate())
                    .setBagNo(newBagNo)
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setItemId(oldBagCode.getItemId())
                    .setItemCode(oldBagCode.getItemCode())
                    .setItemName(oldBagCode.getItemName())
                    .setPalletId(pallet == null ? null : pallet.getId())
                    .setPalletCode(pallet == null ? null : pallet.getCode())
                    .setStatus(MesProBagCodeStatusEnum.GENERATED.getStatus())
@@ -234,17 +410,26 @@
        if (CollUtil.isEmpty(bagCodes)) {
            throw exception(PRO_BAG_CODE_UUID_NOT_EXISTS);
        }
        // 2. æ ¡éªŒå‡ä¸ºå·²ç”ŸæˆçŠ¶æ€ä¸”æœªç»‘å®šæ‰˜ç›˜
        // 2. æ ¡éªŒå‡ä¸ºå·²ç”ŸæˆçŠ¶æ€
        if (bagCodes.stream().anyMatch(bagCode ->
                !MesProBagCodeStatusEnum.GENERATED.getStatus().equals(bagCode.getStatus()))) {
            throw exception(PRO_BAG_CODE_STATUS_NOT_GENERATED);
        }
        if (bagCodes.stream().anyMatch(bagCode -> bagCode.getPalletId() != null)) {
            throw exception(PRO_BAG_CODE_BIND_PALLET_BOUND);
        // 3. ç»‘定的托盘仅限"自动生成且未绑定批次"(生成数据包时自动绑定),级联删除;已入批次的托盘不允许随包删除
        Set<Long> palletIds = new LinkedHashSet<>();
        bagCodes.stream().map(MesProBagCodeDO::getPalletId).filter(Objects::nonNull).forEach(palletIds::add);
        for (Long palletId : palletIds) {
            MesProPalletDO pallet = palletMapper.selectById(palletId);
            if (pallet != null && (pallet.getBatchId() != null || StrUtil.isNotEmpty(pallet.getCode()))) {
                throw exception(PRO_BAG_CODE_BIND_PALLET_BOUND);
            }
        }
        // 3. åˆ é™¤ç”Ÿæˆç»„
        // 4. åˆ é™¤ç”Ÿæˆç»„与其自动生成的临时托盘
        bagCodeMapper.deleteByIds(bagCodes.stream().map(MesProBagCodeDO::getId).toList());
        // 4. å›žå‡æ‰¹æ¬¡è¢‹ç æ•°é‡
        if (!palletIds.isEmpty()) {
            palletMapper.deleteByIds(palletIds);
        }
        // 5. å›žå‡æ‰¹æ¬¡è¢‹ç æ•°é‡
        MesProBatchDO batch = batchMapper.selectById(bagCodes.get(0).getBatchId());
        if (batch != null && batch.getBagQuantity() != null) {
            batchMapper.updateById(new MesProBatchDO().setId(batch.getId())
@@ -262,13 +447,8 @@
        }
        // 2. é”å®šå¹¶æ ¡éªŒè¢‹ç 
        List<MesProBagCodeDO> bagCodes = validateBagCodesForBind(bindReqVO.getIds(), pallet, true);
        // 3. æ ¡éªŒä¸è¶…过托盘可装袋数
        // 3. ç»‘定袋码并累加托盘已装袋数(托盘无容量上限)
        int currentCount = pallet.getBagCount() == null ? 0 : pallet.getBagCount();
        if (pallet.getCapacity() != null && pallet.getCapacity() > 0
                && currentCount + bagCodes.size() > pallet.getCapacity()) {
            throw exception(PRO_BAG_CODE_BIND_PALLET_OVER_CAPACITY);
        }
        // 4. ç»‘定袋码并累加托盘已装袋数
        bagCodeMapper.bindPalletByIds(bindReqVO.getIds(), pallet.getId(), pallet.getCode());
        palletService.updatePalletBagCount(pallet.getId(), currentCount + bagCodes.size());
    }
@@ -354,6 +534,16 @@
    }
    @Override
    public List<MesProBagCodeDO> getPrintableBagCodeListByBatchId(Long batchId) {
        return bagCodeMapper.selectPrintableListByBatchId(batchId);
    }
    @Override
    public List<MesProBagCodeDO> getPrintableBagCodeListByUuid(String uuid) {
        return bagCodeMapper.selectPrintableListByUuid(uuid);
    }
    @Override
    public void markOutbound(Long id, Long salesId) {
        bagCodeMapper.markOutbound(id, salesId, LocalDateTime.now());
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/batch/MesProBatchServiceImpl.java
@@ -7,7 +7,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPreGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletBindBatchReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchWorkerDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.line.MesProLineDO;
@@ -15,11 +15,12 @@
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchWorkerMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.line.MesProLineService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
@@ -59,11 +60,15 @@
    private MesProPalletMapper palletMapper;
    @Resource
    private MesProPalletService palletService;
    @Resource
    private MesCodeRedisDAO codeRedisDao;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MesProLineService lineService;
    @Resource
@@ -72,27 +77,30 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createBatch(MesProBatchSaveReqVO createReqVO) {
        // 1. æ ¡éªŒå…³è”数据(品种、产线、生产人员)
        MesMdmItemDO item = mdmItemService.validateItemExists(createReqVO.getItemId());
        // 1. æ ¡éªŒå…³è”数据(产线、生产人员)
        MesProLineDO line = lineService.validateLineExistsAndEnable(createReqVO.getLineId());
        validateRelations(createReqVO);
        // 2. ç”Ÿæˆæ‰¹æ¬¡å·ï¼ˆç¼–码规则:前缀 + æ—¥æœŸ + äº§çº¿ç¼–码 + å“ç§ç¼–码 + æµæ°´å·ï¼‰
        String code = generateBatchCode(line.getCode(), item.getCode());
        // 2. ç”Ÿæˆæ‰¹æ¬¡å·ï¼ˆç¼–码规则:前缀 + æ—¥æœŸ + äº§çº¿ç¼–码 + æµæ°´å·ï¼‰
        String code = generateBatchCode(line.getCode());
        // 3. æ’入批次(产线编码/名称冗余回填)
        // 3. æ’入批次(产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中)
        boolean bindPallets = CollUtil.isNotEmpty(createReqVO.getPalletIds());
        MesProBatchDO batch = BeanUtils.toBean(createReqVO, MesProBatchDO.class)
                .setCode(code)
                .setLineCode(line.getCode())
                .setLineName(line.getName())
                .setBagQuantity(0)
                .setPalletQuantity(0)
                .setStatus(MesProBatchStatusEnum.DRAFT.getStatus());
                .setStatus(bindPallets ? MesProBatchStatusEnum.PRODUCING.getStatus()
                        : MesProBatchStatusEnum.DRAFT.getStatus());
        batchMapper.insert(batch);
        // 4. æ’入生产人员配置
        insertBatchWorkers(batch.getId(), createReqVO.getWorkers());
        // 5. ç”Ÿæˆæ‰¹æ¬¡å®Œæ•´æ€§æ‘˜è¦
        // 5. ç»‘定托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(batch.getId(), createReqVO.getPalletIds());
        // 6. ç”Ÿæˆæ‰¹æ¬¡å®Œæ•´æ€§æ‘˜è¦
        integrityService.recordBatchIntegrity(batch.getId());
        return batch.getId();
    }
@@ -105,7 +113,7 @@
        List<MesProBatchDO> list = new ArrayList<>(reqVO.getCount());
        for (int i = 0; i < reqVO.getCount(); i++) {
            list.add(new MesProBatchDO()
                    .setTempCode("PB-" + StrUtil.padPre(String.valueOf(startSeq + i), 8, '0'))
                    .setTempCode(codeRedisDao.generate())
                    .setBagQuantity(0)
                    .setPalletQuantity(0)
                    .setStatus(MesProBatchStatusEnum.DRAFT.getStatus())
@@ -130,21 +138,24 @@
            throw exception(PRO_BATCH_ALREADY_FILLED);
        }
        // 2. æ ¡éªŒå…³è”数据并生成正式批次号
        MesMdmItemDO item = mdmItemService.validateItemExists(fillReqVO.getItemId());
        MesProLineDO line = lineService.validateLineExistsAndEnable(fillReqVO.getLineId());
        validateRelations(fillReqVO);
        String code = generateBatchCode(line.getCode(), item.getCode());
        // 3. è½åº“业务信息(保留临时批次码用于历史扫码追溯;产线编码/名称冗余回填)
        String code = generateBatchCode(line.getCode());
        // 3. è½åº“业务信息(保留临时批次码用于历史扫码追溯;产线编码/名称冗余回填;勾选绑定托盘时批次直接进入生产中)
        boolean bindPallets = CollUtil.isNotEmpty(fillReqVO.getPalletIds());
        MesProBatchDO updateObj = BeanUtils.toBean(fillReqVO, MesProBatchDO.class)
                .setCode(code)
                .setLineCode(line.getCode())
                .setLineName(line.getName())
                .setStatus(MesProBatchStatusEnum.DRAFT.getStatus());
                .setStatus(bindPallets ? MesProBatchStatusEnum.PRODUCING.getStatus()
                        : MesProBatchStatusEnum.DRAFT.getStatus());
        batchMapper.updateById(updateObj);
        // 4. é‡å»ºç”Ÿäº§äººå‘˜é…ç½®
        batchWorkerMapper.deleteByBatchId(fillReqVO.getId());
        insertBatchWorkers(fillReqVO.getId(), fillReqVO.getWorkers());
        // 5. ç”Ÿæˆæ‰¹æ¬¡å®Œæ•´æ€§æ‘˜è¦ï¼ˆè¡¥å½•正式信息后覆盖)
        // 5. ç»‘定托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(fillReqVO.getId(), fillReqVO.getPalletIds());
        // 6. ç”Ÿæˆæ‰¹æ¬¡å®Œæ•´æ€§æ‘˜è¦ï¼ˆè¡¥å½•正式信息后覆盖)
        integrityService.recordBatchIntegrity(fillReqVO.getId());
    }
@@ -157,29 +168,33 @@
        // 2. æ ¡éªŒå…³è”数据(生产人员)
        validateRelations(updateReqVO);
        // 3. æœªç”Ÿæˆæ­£å¼æ‰¹æ¬¡å·æˆ–品种/产线变化时重新生成批次号(批次号包含产线编码 + å“ç§ç¼–码要素)
        // 3. æœªç”Ÿæˆæ­£å¼æ‰¹æ¬¡å·æˆ–产线变化时重新生成批次号(批次号包含产线编码要素)
        boolean codeEmpty = StrUtil.isEmpty(oldBatch.getCode());
        boolean itemChanged = !Objects.equals(oldBatch.getItemId(), updateReqVO.getItemId());
        boolean lineChanged = !Objects.equals(oldBatch.getLineId(), updateReqVO.getLineId());
        String code = oldBatch.getCode();
        MesProBatchDO updateObj = BeanUtils.toBean(updateReqVO, MesProBatchDO.class);
        if (codeEmpty || itemChanged || lineChanged) {
            MesMdmItemDO item = mdmItemService.validateItemExists(updateReqVO.getItemId());
        if (codeEmpty || lineChanged) {
            // å˜æ›´äº§çº¿æ—¶ï¼Œç›®æ ‡äº§çº¿å¿…须启用;未变更则仅校验存在(避免停用产线的历史批次无法编辑其他字段)
            MesProLineDO line = (lineChanged || codeEmpty)
                    ? lineService.validateLineExistsAndEnable(updateReqVO.getLineId())
                    : lineService.validateLineExists(updateReqVO.getLineId());
            code = generateBatchCode(line.getCode(), item.getCode());
            code = generateBatchCode(line.getCode());
            updateObj.setLineCode(line.getCode()).setLineName(line.getName());
        }
        // 4. æ›´æ–°æ‰¹æ¬¡
        // 4. æ›´æ–°æ‰¹æ¬¡ï¼ˆå‹¾é€‰ç»‘定托盘时批次直接进入生产中;取消勾选不回退,已绑定托盘不可解绑)
        boolean bindPallets = CollUtil.isNotEmpty(updateReqVO.getPalletIds());
        updateObj.setCode(code);
        if (bindPallets) {
            updateObj.setStatus(MesProBatchStatusEnum.PRODUCING.getStatus());
        }
        batchMapper.updateById(updateObj);
        // 5. é‡å»ºç”Ÿäº§äººå‘˜é…ç½®
        batchWorkerMapper.deleteByBatchId(updateReqVO.getId());
        insertBatchWorkers(updateReqVO.getId(), updateReqVO.getWorkers());
        // 6. æ‘˜è¦è¦†ç›–范围含品种/产线/生产日期/计划数量,编辑后重新生成避免误报篡改
        // 6. ç»‘定新增勾选的托盘(级联生成正式托盘码并录入托盘下袋码,失败整体回滚)
        bindPalletsToBatch(updateReqVO.getId(), updateReqVO.getPalletIds());
        // 7. æ‘˜è¦è¦†ç›–范围含品种/产线/生产日期/计划数量,编辑后重新生成避免误报篡改
        integrityService.recordBatchIntegrity(updateReqVO.getId());
    }
@@ -283,7 +298,7 @@
    /**
     * æ ¡éªŒå…³è”数据(生产人员:责任人 + ç”Ÿäº§äººå‘˜é…ç½®ï¼‰
     * <p>
     * å“ç§ã€äº§çº¿çš„æ ¡éªŒåœ¨åˆ›å»º/更新流程中内联执行(需取编码参与批次号生成)
     * äº§çº¿çš„æ ¡éªŒåœ¨åˆ›å»º/更新流程中内联执行(需取编码参与批次号生成)
     */
    private void validateRelations(MesProBatchSaveReqVO reqVO) {
        Set<Long> userIds = new HashSet<>();
@@ -293,16 +308,29 @@
    }
    /**
     * æ‰¹æ¬¡ç»‘定托盘(可选):同事务内复用托盘绑定批次逻辑,
     * ç”Ÿæˆæ­£å¼æ‰˜ç›˜ç å¹¶çº§è”录入托盘下未绑批次的袋码,失败整体回滚
     */
    private void bindPalletsToBatch(Long batchId, List<Long> palletIds) {
        if (CollUtil.isEmpty(palletIds)) {
            return;
        }
        MesProPalletBindBatchReqVO bindReqVO = new MesProPalletBindBatchReqVO();
        bindReqVO.setBatchId(batchId);
        bindReqVO.setIds(palletIds);
        palletService.bindBatch(bindReqVO);
    }
    /**
     * ç”Ÿæˆæ‰¹æ¬¡å·
     * <p>
     * ç¼–码规则:固定前缀(PC) + æ—¥æœŸ(yyyyMMdd) + è¾“入字符(产线编码 + å“ç§ç¼–码) + æµæ°´å·(按日循环)
     * ç¼–码规则:固定前缀(PC) + æ—¥æœŸ(yyyyMMdd) + è¾“入字符(产线编码) + æµæ°´å·(按日循环)
     *
     * @param lineCode äº§çº¿ç¼–码
     * @param itemCode å“ç§ï¼ˆç‰©æ–™ï¼‰ç¼–码
     * @return æ‰¹æ¬¡å·
     */
    private String generateBatchCode(String lineCode, String itemCode) {
        String inputChar = StrUtil.emptyToDefault(lineCode, "") + StrUtil.emptyToDefault(itemCode, "");
    private String generateBatchCode(String lineCode) {
        String inputChar = StrUtil.emptyToDefault(lineCode, "");
        return autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_BATCH_CODE.getCode(), inputChar);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/pallet/MesProPalletService.java
@@ -90,6 +90,13 @@
    List<MesProPalletDO> getPalletListByBatchId(Long batchId);
    /**
     * èŽ·å¾—æœªç»‘å®šç”Ÿäº§æ‰¹æ¬¡çš„ä¸´æ—¶æ‰˜ç›˜åˆ—è¡¨ï¼ˆä¾›æ‰¹æ¬¡æ–°å¢ž/补录绑定托盘下拉使用)
     *
     * @return æ‰˜ç›˜ç åˆ—表
     */
    List<MesProPalletDO> getUnboundPalletList();
    /**
     * èŽ·å¾—æ‰˜ç›˜ç åˆ—è¡¨ï¼ˆæŒ‰ç¼–å·åˆ—è¡¨ï¼‰
     *
     * @param ids ç¼–号列表
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/pallet/MesProPalletServiceImpl.java
@@ -7,11 +7,14 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletBindBatchReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletPreGenerateReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
import cn.iocoder.yudao.module.mes.dal.redis.MesCodeRedisDAO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
import jakarta.annotation.Resource;
@@ -22,7 +25,9 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -43,6 +48,9 @@
    private MesProBagCodeMapper bagCodeMapper;
    @Resource
    private MesProBatchMapper batchMapper;
    @Resource
    private MesCodeRedisDAO codeRedisDao;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -67,11 +75,10 @@
        for (int i = 0; i < createReqVO.getCount(); i++) {
            int palletNo = startNo + i;
            pallets.add(new MesProPalletDO()
                    .setCode(batch.getCode() + "-P" + StrUtil.padPre(String.valueOf(palletNo), 2, '0'))
                    .setCode(codeRedisDao.generate())
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setPalletNo(palletNo)
                    .setCapacity(createReqVO.getCapacity())
                    .setBagCount(0)
                    .setAutoGenerated(false)
                    .setStatus(MesProPalletStatusEnum.IN_USE.getStatus())
@@ -94,8 +101,7 @@
        List<MesProPalletDO> list = new ArrayList<>(reqVO.getCount());
        for (int i = 0; i < reqVO.getCount(); i++) {
            list.add(new MesProPalletDO()
                    .setTempCode("PL-" + StrUtil.padPre(String.valueOf(startSeq + i), 8, '0'))
                    .setCapacity(reqVO.getCapacity())
                    .setTempCode(codeRedisDao.generate())
                    .setBagCount(0)
                    .setAutoGenerated(false)
                    .setStatus(MesProPalletStatusEnum.IN_USE.getStatus())
@@ -136,20 +142,50 @@
            }
            pallets.add(pallet);
        }
        // 3. åˆ†é…æ­£å¼æ‰˜ç›˜ç ï¼šæ‰¹æ¬¡å· + -P + 2 ä½æ‰˜ç›˜åºå·ï¼ˆå«é€»è¾‘删除记录参与序号,保证不回退、码值唯一)
        // 3. çº§è”准备:托盘下未绑批次的袋码随托盘一起录入批次,品种须与批次已有袋码一致
        List<MesProBagCodeDO> bags = bagCodeMapper.selectUnboundListByPalletIds(ids);
        List<Long> bindBagIds = new ArrayList<>();
        Set<Long> bagItemIds = new LinkedHashSet<>();
        for (MesProBagCodeDO bag : bags) {
            if (!MesProBagCodeStatusEnum.GENERATED.getStatus().equals(bag.getStatus())
                    && !MesProBagCodeStatusEnum.PRINTED.getStatus().equals(bag.getStatus())) {
                continue;
            }
            bindBagIds.add(bag.getId());
            if (bag.getItemId() != null) {
                bagItemIds.add(bag.getItemId());
            }
        }
        if (!bindBagIds.isEmpty()) {
            bagItemIds.addAll(bagCodeMapper.selectDistinctItemIdsByBatchId(batch.getId()));
            if (bagItemIds.size() > 1) {
                throw exception(PRO_BAG_CODE_BIND_BATCH_ITEM_MISMATCH);
            }
        }
        // 4. åˆ†é…æ­£å¼æ‰˜ç›˜ç ï¼šæ‰¹æ¬¡å· + -P + 2 ä½æ‰˜ç›˜åºå·ï¼ˆå«é€»è¾‘删除记录参与序号,保证不回退、码值唯一)
        int startNo = palletMapper.selectMaxPalletNo(batch.getId()) + 1;
        for (int i = 0; i < pallets.size(); i++) {
            int palletNo = startNo + i;
            palletMapper.updateById(new MesProPalletDO()
                    .setId(pallets.get(i).getId())
                    .setCode(batch.getCode() + "-P" + StrUtil.padPre(String.valueOf(palletNo), 2, '0'))
                    .setCode(codeRedisDao.generate())
                    .setBatchId(batch.getId())
                    .setBatchCode(batch.getCode())
                    .setPalletNo(palletNo));
            // æ‰˜ç›˜ä¸‹è¢‹ç çš„冗余托盘码由临时码更新为正式码
            bagCodeMapper.updatePalletCodeByPalletId(pallets.get(i).getId(),
                    batch.getCode() + "-P" + StrUtil.padPre(String.valueOf(palletNo), 2, '0'));
        }
        // 4. å›žå†™æ‰¹æ¬¡æ‰˜ç›˜ç æ•°é‡
        // 5. æ‰˜ç›˜ä¸‹è¢‹ç æ‰¹é‡ç»‘定本批次
        if (!bindBagIds.isEmpty() && bagCodeMapper.bindBatchByIds(bindBagIds, batch.getId(), batch.getCode(),
                List.of(MesProBagCodeStatusEnum.GENERATED.getStatus(),
                        MesProBagCodeStatusEnum.PRINTED.getStatus())) != bindBagIds.size()) {
            throw exception(PRO_BAG_CODE_STATUS_NOT_GENERATED);
        }
        // 6. å›žå†™æ‰¹æ¬¡æ‰˜ç›˜ç æ•°é‡ä¸Žæœ‰æ•ˆè¢‹ç æ•°é‡
        batchMapper.updateById(new MesProBatchDO().setId(batch.getId())
                .setPalletQuantity((batch.getPalletQuantity() == null ? 0 : batch.getPalletQuantity()) + pallets.size()));
                .setPalletQuantity((batch.getPalletQuantity() == null ? 0 : batch.getPalletQuantity()) + pallets.size())
                .setBagQuantity(bagCodeMapper.selectEffectiveCountByBatchId(batch.getId())));
        return pallets.size();
    }
@@ -206,6 +242,11 @@
    }
    @Override
    public List<MesProPalletDO> getUnboundPalletList() {
        return palletMapper.selectUnboundList();
    }
    @Override
    public List<MesProPalletDO> getPalletListByIds(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return List.of();
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventServiceImpl.java
@@ -5,11 +5,13 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.scan.MesProScanEventMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesProScanEventStatusEnum;
@@ -20,47 +22,102 @@
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@Validated
public class MesProScanEventServiceImpl implements MesProScanEventService {
    @Resource private MesProScanEventMapper eventMapper;
    @Resource private MesProBagCodeMapper bagCodeMapper;
    @Resource private MesProBatchMapper batchMapper;
    @Resource private MesProPalletMapper palletMapper;
    @Resource private MesWmTransactionService transactionService;
    @Resource private MesWmWarehouseService warehouseService;
    @Resource private MesWmBatchService mesWmBatchService;
    /** æ‰«ç ç±»åž‹ï¼šè¢‹ç ï¼ˆé»˜è®¤ï¼‰ */
    private static final String SCAN_TYPE_BAG = "BAG";
    /** æ‰«ç ç±»åž‹ï¼šæ‰˜ç›˜ç ï¼ˆæŒ‰æ‰˜ç›˜å·²è£…袋数整托入库) */
    private static final String SCAN_TYPE_PALLET = "PALLET";
    /** æ‰«ç ç±»åž‹ï¼šæ‰¹æ¬¡ç ï¼ˆæŒ‰æ‰¹æ¬¡è¢‹æ•°æ•´æ‰¹å…¥åº“) */
    private static final String SCAN_TYPE_BATCH = "BATCH";
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProScanEventDO createInbound(MesProScanEventCreateReqVO req) {
        MesProScanEventDO existing = eventMapper.selectByEventIdForUpdate(req.getEventId());
        if (existing != null) {
            if (!req.getBagCode().equals(existing.getBagCode()) || !req.getDeviceCode().equals(existing.getDeviceCode())) {
            boolean contentMatch = req.getBagCode().equals(existing.getBagCode())
                    && (req.getDeviceCode() == null || req.getDeviceCode().equals(existing.getDeviceCode()));
            if (!contentMatch) {
                return failed(existing, "MES_SCAN_EVENT_ID_CONFLICT", "事件号已存在但内容不一致");
            }
            return existing;
        }
        String scanType = (req.getScanType() == null || req.getScanType().isBlank()) ? SCAN_TYPE_BAG : req.getScanType();
        MesProScanEventDO event = new MesProScanEventDO();
        event.setEventId(req.getEventId()).setBagCode(req.getBagCode()).setQuantity(req.getQuantity())
                .setDeviceCode(req.getDeviceCode()).setLineCode(req.getLineCode()).setRemark(req.getRemark())
                .setStatus(MesProScanEventStatusEnum.FAILED).setReceivedTime(LocalDateTime.now());
        event.setEventId(req.getEventId()).setBagCode(req.getBagCode()).setScanType(scanType)
                .setQuantity(req.getQuantity()).setDeviceCode(req.getDeviceCode()).setLineCode(req.getLineCode())
                .setRemark(req.getRemark()).setStatus(MesProScanEventStatusEnum.FAILED).setReceivedTime(LocalDateTime.now());
        eventMapper.insert(event);
        MesProBagCodeDO bag = bagCodeMapper.selectByCodeForUpdate(req.getBagCode());
        if (bag == null) return failed(event, "MES_SCAN_BAG_NOT_EXISTS", "袋码不存在");
        event.setBagCodeId(bag.getId());
        if (bag.getInboundEventId() != null) return failed(event, "MES_SCAN_BAG_ALREADY_INBOUND", "袋码已入库");
        MesProBatchDO batch = batchMapper.selectByIdForUpdate(bag.getBatchId());
        if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "生产批次不存在");
        event.setBatchId(batch.getId()).setBatchCode(batch.getCode()).setItemId(batch.getItemId());
        // 1. æŒ‰æ‰«ç ç±»åž‹è§£æžæ‰«ç ç›®æ ‡ä¸Žå…¥åº“数量(托盘按已装袋数、批次按批次袋数整批/整托入库)
        Long targetId;
        Long batchId;
        BigDecimal quantity;
        MesProBatchDO batch;
        // å•袋扫码入库时传递袋码,供库存袋码明细在入库第一时间建立;托盘/批次整批入库为批量,不逐袋建明细
        Long bagCodeId = null;
        String bagCode = null;
        if (SCAN_TYPE_PALLET.equals(scanType)) {
            MesProPalletDO pallet = palletMapper.selectByCode(req.getBagCode());
            if (pallet == null) {
                pallet = palletMapper.selectByTempCode(req.getBagCode());
            }
            if (pallet == null) return failed(event, "MES_SCAN_PALLET_NOT_EXISTS", "托盘码不存在");
            if (pallet.getInboundEventId() != null) return failed(event, "MES_SCAN_PALLET_ALREADY_INBOUND", "托盘已入库");
            batch = batchMapper.selectByIdForUpdate(pallet.getBatchId());
            if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "生产批次不存在");
            batchId = batch.getId();
            targetId = pallet.getId();
            quantity = pallet.getBagCount() != null && pallet.getBagCount() > 0
                    ? BigDecimal.valueOf(pallet.getBagCount())
                    : BigDecimal.valueOf(bagCodeMapper.selectCountByPalletId(pallet.getId()));
        } else if (SCAN_TYPE_BATCH.equals(scanType)) {
            batch = batchMapper.selectByCode(req.getBagCode());
            if (batch == null) {
                batch = batchMapper.selectByTempCode(req.getBagCode());
            }
            if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "生产批次不存在");
            if (batch.getInboundEventId() != null) return failed(event, "MES_SCAN_BATCH_ALREADY_INBOUND", "批次已整批入库");
            batchId = batch.getId();
            targetId = batch.getId();
            quantity = batch.getBagQuantity() != null && batch.getBagQuantity() > 0
                    ? BigDecimal.valueOf(batch.getBagQuantity())
                    : BigDecimal.valueOf(bagCodeMapper.selectCountByBatchId(batch.getId()));
        } else {
            MesProBagCodeDO bag = bagCodeMapper.selectByCodeForUpdate(req.getBagCode());
            if (bag == null) return failed(event, "MES_SCAN_BAG_NOT_EXISTS", "袋码不存在");
            event.setBagCodeId(bag.getId());
            bagCodeId = bag.getId();
            bagCode = req.getBagCode();
            if (bag.getInboundEventId() != null) return failed(event, "MES_SCAN_BAG_ALREADY_INBOUND", "袋码已入库");
            batch = batchMapper.selectByIdForUpdate(bag.getBatchId());
            if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "生产批次不存在");
            batchId = batch.getId();
            targetId = bag.getId();
            quantity = req.getQuantity();
        }
        event.setBatchId(batchId).setBatchCode(batch.getCode()).setItemId(batch.getItemId());
        MesWmWarehouseDO warehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
        if (warehouse == null) return failed(event, "MES_SCAN_WIP_WAREHOUSE_NOT_EXISTS", "WIP虚拟仓不存在");
        event.setWarehouseId(warehouse.getId()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId());
@@ -72,19 +129,31 @@
            }
            Long txId = transactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.IN.getType()).setBizType(MesBizTypeConstants.WM_PRODUCT_PRODUCE)
                    .setBizId(batch.getId()).setBizCode(batch.getCode()).setBizLineId(bag.getId())
                    .setItemId(batch.getItemId()).setQuantity(req.getQuantity())
                    .setBizId(batchId).setBizCode(batch.getCode()).setBizLineId(targetId)
                    .setItemId(batch.getItemId()).setQuantity(quantity)
                    .setBatchId(wmBatch.getId()).setBatchCode(batch.getCode())
                    .setBagCodeId(bagCodeId).setBagCode(bagCode)
                    .setWarehouseId(warehouse.getId()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId())
                    .setReceiptTime(LocalDateTime.now()).setCheckFlag(true)
                    .setStockState(MesWmStockStateEnum.TEMP.getState()));
            event.setTransactionId(txId).setStatus(MesProScanEventStatusEnum.SUCCESS).setEventTime(LocalDateTime.now());
            eventMapper.updateById(event);
            bagCodeMapper.markInbound(bag.getId(), event.getId(), req.getDeviceCode(), req.getLineCode());
            batchMapper.markInbound(batch.getId(), event.getId(), req.getDeviceCode(), req.getLineCode());
            // 2. å›žå†™å…¥åº“标记(托盘/批次整托/整批入库时同步标记其下未入库袋码,保持状态一致)
            if (SCAN_TYPE_PALLET.equals(scanType)) {
                palletMapper.markInbound(targetId, event.getId());
                bagCodeMapper.markInboundByPalletId(targetId, event.getId(), req.getLineCode());
            } else if (SCAN_TYPE_BATCH.equals(scanType)) {
                batchMapper.markBatchInbound(targetId, event.getId(), req.getLineCode(), batch.getBagQuantity());
                bagCodeMapper.markInboundByBatchId(targetId, event.getId(), req.getLineCode());
            } else {
                bagCodeMapper.markInbound(targetId, event.getId(), req.getDeviceCode(), req.getLineCode());
                batchMapper.markInbound(batchId, event.getId(), req.getDeviceCode(), req.getLineCode());
            }
            return event;
        } catch (Exception ex) {
            return failed(event, "MES_SCAN_TRANSACTION_FAILED", "库存事务处理失败");
            log.error("[createInbound] åº“存事务处理失败,eventId={}, bagCode={}, batchId={}, itemId={}",
                    req.getEventId(), req.getBagCode(), batchId, batch.getItemId(), ex);
            throw ex;
        }
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java
@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.mes.service.qc.seedquality;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityIndicatorDO;
@@ -10,6 +12,8 @@
    Long submit(MesQcSeedQualitySubmitReqVO reqVO);
    PageResult<MesQcSeedQualityDO> getQualityPage(MesQcSeedQualityPageReqVO reqVO);
    /**
     * æ ¹æ®æ‰¹æ¬¡å·æŸ¥è¯¢è´¨æ£€è®°å½•(按检验时间倒序,取最新),用于公开溯源。
     * ç§å­è´¨æ£€è¡¨æœªåˆå§‹åŒ–时返回空列表(容错,不影响追溯)。
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
@@ -1,41 +1,117 @@
package cn.iocoder.yudao.module.mes.service.qc.seedquality;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.*;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.materialstockbag.MesWmMaterialStockBagService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_NOT_EXISTS;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_TYPE_NOT_QUALIFIED;
@Service @Validated @Slf4j
public class MesQcSeedQualityServiceImpl implements MesQcSeedQualityService {
    private static final Set<String> REQUIRED = Set.of("purity", "cleanliness", "germination_rate", "moisture");
    @Resource private MesQcSeedQualityMapper qualityMapper;
    @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper;
    @Resource private MesProBagCodeMapper bagCodeMapper;
    @Resource private MesProBatchService proBatchService;
    @Resource private MesWmBatchService mesWmBatchService;
    @Resource private MesWmMaterialStockService materialStockService;
    @Resource private MesWmBatchMapper mesWmBatchMapper;
    @Resource private MesWmMaterialStockMapper materialStockMapper;
    @Resource private MesWmMaterialStockBagMapper materialStockBagMapper;
    @Resource private MesWmMaterialStockBagService materialStockBagService;
    @Resource private MesWmTransactionService wmTransactionService;
    @Resource private MesWmWarehouseService warehouseService;
    @Resource private MesWmWarehouseLocationService warehouseLocationService;
    @Resource private MesWmWarehouseAreaService warehouseAreaService;
    @Override
    public PageResult<MesQcSeedQualityDO> getQualityPage(MesQcSeedQualityPageReqVO reqVO) {
        return qualityMapper.selectPage(reqVO);
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public Long submit(MesQcSeedQualitySubmitReqVO reqVO) {
        if (reqVO.getIndicators().size() != 4) throw new IllegalArgumentException("必须提交四项指标");
        if (qualityMapper.selectByCode(reqVO.getCode()) != null) throw new IllegalArgumentException("该袋码已质检,禁止重复提交");
        // ç§å­è´¨æ£€å¿…须以袋码(单袋)提交,物料信息由袋码自动解析,前端不再单独选择物料
        MesProBagCodeDO bag = bagCodeMapper.selectByCode(reqVO.getCode());
        if (bag == null) {
            throw new IllegalArgumentException("未解析到有效袋码,种子质检必须以袋码提交");
        }
        MesProBatchDO batch = resolveBatch(reqVO.getCode(), reqVO.getBatchCode());
        if (batch == null || batch.getItemId() == null) {
            throw new IllegalArgumentException("未根据袋码解析到物料信息,请确认扫码内容或批次号正确");
        }
        // è¢‹ç å¯¹åº”批次的库存必须处于暂存库(待质检)中,才能进行种子质检
        MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode());
        if (wmBatch == null || materialStockMapper.selectTempStockCountByBatchId(wmBatch.getId()) == 0) {
            throw new IllegalArgumentException("该袋码/批次对应的库存不在暂存库中,无法进行质检");
        }
        // æ ¡éªŒç›®æ ‡åˆæ ¼åº“:必须存在且仓库类型为合格库(20)
        if (warehouseService.getFirstQualifiedWarehouse() == null) {
            throw exception(WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED);
        }
        MesWmWarehouseDO qualifiedWarehouse = warehouseService.getWarehouse(reqVO.getQualifiedWarehouseId());
        if (qualifiedWarehouse == null) {
            throw exception(WM_WAREHOUSE_NOT_EXISTS);
        }
        if (!MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())) {
            throw exception(WM_WAREHOUSE_TYPE_NOT_QUALIFIED);
        }
        // è§£æžåˆæ ¼åº“默认库区/库位(取该仓库下第一个库区、其下第一个库位,未配置则为 null)
        Long qualifiedLocationId = null;
        Long qualifiedAreaId = null;
        List<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId());
        if (CollUtil.isNotEmpty(locations)) {
            qualifiedLocationId = locations.get(0).getId();
            List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(qualifiedLocationId);
            if (CollUtil.isNotEmpty(areas)) {
                qualifiedAreaId = areas.get(0).getId();
            }
        }
        Set<String> codes = new HashSet<>();
        boolean passed = true;
        MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(reqVO.getCode()).itemId(reqVO.getItemId())
                .batchCode(reqVO.getBatchCode()).reason(reqVO.getReason()).result(0)
        MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(reqVO.getCode()).itemId(batch.getItemId())
                .batchCode(batch.getCode()).reason(reqVO.getReason()).result(0)
                .inspectedAt(LocalDateTime.now()).build();
        qualityMapper.insert(quality);
        for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) {
@@ -47,20 +123,151 @@
        if (!codes.equals(REQUIRED)) throw new IllegalArgumentException("四项指标不完整");
        quality.setResult(passed ? 1 : 2);
        qualityMapper.updateById(quality);
        // è´¨æ£€åˆæ ¼ â†’ åº“存整批由暂存库(待质检)转为合格库(正式库存)
        // æŒ‰è¢‹ç ç²’度流转库存:锁定该袋在暂存库中的活动明细,禁止按批次任意扣减
        MesWmMaterialStockBagDO stockBag = materialStockBagMapper.selectActiveByBagCode(bag.getCode());
        MesWmMaterialStockDO tempStock = resolveTempStock(bag, wmBatch, stockBag);
        // A. æš‚存出库(-1):该袋离开暂存库,写入质检暂存出库事务(跳过袋码自动维护,由质检统一控制袋码最终落点)
        MesWmTransactionSaveReqDTO tempOutReq = new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.OUT.getType())
                .setBizType(MesBizTypeConstants.QC_SEED_QUALITY)
                .setBizId(quality.getId())
                .setBizCode(reqVO.getCode())
                .setBizLineId(quality.getId())
                .setItemId(batch.getItemId())
                .setQuantity(BigDecimal.ONE.negate())
                .setBatchId(tempStock.getBatchId())
                .setBatchCode(tempStock.getBatchCode())
                .setBagCodeId(bag.getId())
                .setBagCode(bag.getCode())
                .setWarehouseId(tempStock.getWarehouseId())
                .setLocationId(tempStock.getLocationId())
                .setAreaId(tempStock.getAreaId())
                .setVendorId(tempStock.getVendorId())
                .setMaterialStockId(tempStock.getId())
                .setCheckFlag(true)
                .setSkipBagManage(true);
        Long tempOutTxId = wmTransactionService.createTransaction(tempOutReq);
        if (passed) {
            MesProBatchDO proBatch = proBatchService.getBatchByCode(reqVO.getBatchCode());
            if (proBatch != null) {
                MesWmBatchDO wmBatch = mesWmBatchService.getOrCreateByProBatch(proBatch);
                if (wmBatch != null) {
                    int updated = materialStockService.updateStockStateByBatchId(wmBatch.getId(),
                            MesWmStockStateEnum.QUALIFIED.getState(), MesWmStockStateEnum.TEMP.getState());
                    log.info("[submit][质检合格,批次 {} åº“存由暂存库转合格库,影响 {} æ¡]", reqVO.getBatchCode(), updated);
            // B1. è´¨æ£€åˆæ ¼ â†’ è¯¥è¢‹è½¬å…¥æ“ä½œå‘˜é€‰æ‹©çš„合格库,写入合格入库事务(跳过袋码自动维护)
            MesWmMaterialStockDO qualifiedStock = getOrCreateQualifiedStock(tempStock, qualifiedWarehouse.getId(), qualifiedLocationId, qualifiedAreaId);
            MesWmTransactionSaveReqDTO qualInReq = new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.IN.getType())
                    .setBizType(MesBizTypeConstants.QC_SEED_QUALITY)
                    .setBizId(quality.getId())
                    .setBizCode(reqVO.getCode())
                    .setBizLineId(quality.getId())
                    .setItemId(batch.getItemId())
                    .setQuantity(BigDecimal.ONE)
                    .setBatchId(qualifiedStock.getBatchId())
                    .setBatchCode(qualifiedStock.getBatchCode())
                    .setBagCodeId(bag.getId())
                    .setBagCode(bag.getCode())
                    .setWarehouseId(qualifiedStock.getWarehouseId())
                    .setLocationId(qualifiedStock.getLocationId())
                    .setAreaId(qualifiedStock.getAreaId())
                    .setVendorId(qualifiedStock.getVendorId())
                    .setMaterialStockId(qualifiedStock.getId())
                    .setCheckFlag(false)
                    .setStockState(MesWmStockStateEnum.QUALIFIED.getState())
                    .setSkipBagManage(true);
            Long qualInTxId = wmTransactionService.createTransaction(qualInReq);
            // B2. åŒæ­¥è¿ç§»è¢‹ç æ˜Žç»†åˆ°åˆæ ¼åº“存落点
            materialStockBagService.moveIn(qualInReq, qualifiedStock, qualInTxId);
            bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.QUALIFIED.getStatus());
            log.info("[submit][质检合格,袋码 {} ç”±æš‚存库转合格库 {}]", bag.getCode(), qualifiedWarehouse.getName());
        } else {
            // C. è´¨æ£€ä¸åˆæ ¼ â†’ è¯¥è¢‹æŠ¥åºŸï¼›æš‚存出库事务已扣减暂存库存,标记袋码报废
            materialStockBagService.scrap(tempOutReq, tempOutTxId);
            bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.UNQUALIFIED.getStatus());
            log.info("[submit][质检不合格,袋码 {} æŠ¥åºŸ]", bag.getCode());
        }
        return quality.getId();
    }
    /**
     * å®šä½è¯¥è¢‹çš„æš‚存库存汇总记录(只定位,不扣减;扣减由质检暂存出库事务完成)。
     * <p>
     * ä¼˜å…ˆä¸¥æ ¼æŒ‰è¢‹ç é”å®šå…¶æ´»åŠ¨æ˜Žç»†å¯¹åº”çš„æš‚å­˜è®°å½•ï¼›åŽ†å²æ— æ˜Žç»†æ—¶å›žé€€æŒ‰æ‰¹æ¬¡å®šä½å¹¶è¡¥å»ºæ˜Žç»†ã€‚
     */
    private MesWmMaterialStockDO resolveTempStock(MesProBagCodeDO bag, MesWmBatchDO wmBatch, MesWmMaterialStockBagDO stockBag) {
        if (stockBag != null && stockBag.getMaterialStockId() != null) {
            MesWmMaterialStockDO stock = materialStockMapper.selectById(stockBag.getMaterialStockId());
            if (stock != null && MesWmStockStateEnum.TEMP.getState().equals(stock.getStockState())) {
                return stock;
            }
        }
        MesWmMaterialStockDO tempStock = materialStockMapper.selectStockListByBatchId(wmBatch.getId()).stream()
                .filter(s -> MesWmStockStateEnum.TEMP.getState().equals(s.getStockState()))
                .max(Comparator.comparing(MesWmMaterialStockDO::getQuantity))
                .orElse(null);
        if (tempStock == null) {
            throw new IllegalArgumentException("该批次暂存库存不足,无法质检");
        }
        getOrCreateStockBag(bag, tempStock);
        return tempStock;
    }
    /**
     * è´¨æ£€åˆæ ¼ï¼šå°†è´¨æ£€é€šè¿‡çš„那袋转入操作员选择的合格库(同组合键合格库记录 +1,不存在则新建)。
     *
     * @param tempStock            ä»Žæš‚存库扣减后返回的暂存记录(携带 item/batch/vendor ç»´åº¦ï¼‰
     * @param qualifiedWarehouseId ç›®æ ‡åˆæ ¼åº“仓库编号
     * @param qualifiedLocationId  ç›®æ ‡åˆæ ¼åº“库区编号(未配置则为 null)
     * @param qualifiedAreaId      ç›®æ ‡åˆæ ¼åº“库位编号(未配置则为 null)
     */
    private MesWmMaterialStockBagDO getOrCreateStockBag(MesProBagCodeDO bag, MesWmMaterialStockDO tempStock) {
        MesWmMaterialStockBagDO stockBag = materialStockBagMapper.selectByBagCode(bag.getCode());
        if (stockBag != null) {
            return stockBag;
        }
        stockBag = MesWmMaterialStockBagDO.builder().materialStockId(tempStock.getId())
                .bagCodeId(bag.getId()).bagCode(bag.getCode()).itemId(tempStock.getItemId())
                .batchId(tempStock.getBatchId()).batchCode(tempStock.getBatchCode()).vendorId(tempStock.getVendorId())
                .warehouseId(tempStock.getWarehouseId()).locationId(tempStock.getLocationId()).areaId(tempStock.getAreaId())
                .stockState(MesWmStockStateEnum.TEMP.getState()).status(1).quantity(BigDecimal.ONE)
                .frozen(false).inboundTime(tempStock.getReceiptTime()).build();
        materialStockBagMapper.insert(stockBag);
        return stockBag;
    }
    private MesWmMaterialStockDO getOrCreateQualifiedStock(MesWmMaterialStockDO tempStock, Long qualifiedWarehouseId,
                                         Long qualifiedLocationId, Long qualifiedAreaId) {
        MesWmMaterialStockDO qualified = materialStockMapper.selectQualifiedByCompositeKey(
                tempStock.getItemId(), qualifiedWarehouseId, qualifiedLocationId,
                qualifiedAreaId, tempStock.getBatchId(), tempStock.getVendorId());
        if (qualified != null) {
            return qualified;
        }
        // ä¸å­˜åœ¨åˆ™æ–°å»ºåˆæ ¼åº“记录(仓库/库区/库位采用所选合格库的维度,数量由质检入库事务 +1)
        MesWmMaterialStockDO newStock = MesWmMaterialStockDO.builder()
                .itemId(tempStock.getItemId()).itemTypeId(tempStock.getItemTypeId())
                .warehouseId(qualifiedWarehouseId).locationId(qualifiedLocationId).areaId(qualifiedAreaId)
                .batchId(tempStock.getBatchId()).batchCode(tempStock.getBatchCode()).vendorId(tempStock.getVendorId())
                .quantity(BigDecimal.ZERO).receiptTime(LocalDateTime.now())
                .stockState(MesWmStockStateEnum.QUALIFIED.getState())
                .frozen(false)
                .build();
        materialStockMapper.insert(newStock);
        return newStock;
    }
    /**
     * ç”±è¢‹ç æˆ–生产批次解析所属生产批次(袋码优先,批次码兼容临时码)
     */
    private MesProBatchDO resolveBatch(String code, String batchCode) {
        if (StrUtil.isNotBlank(code)) {
            MesProBagCodeDO bag = bagCodeMapper.selectByCode(code);
            if (bag != null && bag.getBatchId() != null) {
                MesProBatchDO batch = proBatchService.getBatch(bag.getBatchId());
                if (batch != null) {
                    return batch;
                }
            }
        }
        // åº“位由后台配置服务接管,当前版本保留审计闭环,禁止前端传入库位。
        return quality.getId();
        if (StrUtil.isNotBlank(batchCode)) {
            return proBatchService.getBatchByCode(batchCode);
        }
        return null;
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicService.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.mes.service.trace;
import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicConfigRespVO;
import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicRespVO;
/**
@@ -18,4 +19,14 @@
     */
    MesProTracePublicRespVO queryTrace(String code);
    /**
     * èŽ·å¾— H5 æº¯æºé¡µé…ç½®ï¼ˆç”Ÿäº§ç»è¥è€…名称、追溯网址)
     * <p>
     * ä¸¤é¡¹å‡ä¸ºç§å­æ ‡ç­¾å››è¦ç´ çš„组成部分,由后端统一下发,保证二维码内容、标签印刷内容与
     * H5 é¡µé¢å±•示一致。
     *
     * @return é¡µé¢é…ç½®
     */
    MesProTracePublicConfigRespVO getPublicConfig();
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
@@ -2,9 +2,11 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicConfigRespVO;
import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
@@ -17,6 +19,9 @@
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchWorkerTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
import cn.iocoder.yudao.module.mes.enums.trace.MesTraceCodeStatusEnum;
import cn.iocoder.yudao.module.mes.framework.qrcode.MesProQrLabelBuilder;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
@@ -29,6 +34,9 @@
import cn.iocoder.yudao.module.mes.service.trace.crossregion.MesTraceCrossRegionService;
import cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
@@ -38,8 +46,12 @@
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -75,6 +87,8 @@
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MdmUnitMeasureApi unitMeasureApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesWmBatchService mesWmBatchService;
@@ -97,13 +111,13 @@
        }
        TraceEntry entry = null;
        try {
            // 1. è§£æžè¿½æº¯å…¥å£ï¼Œå®šä½æ‰¹æ¬¡
            // 1. è§£æžè¿½æº¯å…¥å£ï¼Œå®šä½æ‰¹æ¬¡ï¼ˆè¢‹ç /托盘已生成但未录入批次时,降级为仅返回品种信息)
            entry = resolveEntry(traceCode);
            if (entry.batchId == null) {
            if (entry.batchId == null && entry.bagItemId == null) {
                throw exception(MES_TRACE_CODE_NOT_FOUND);
            }
            // 2. èšåˆå…¨ç¨‹ä¿¡æ¯
            MesProTracePublicRespVO result = buildRespVO(rawCode, traceCode, entry.sourceType, entry.batchId);
            // 2. èšåˆå…¨ç¨‹ä¿¡æ¯ï¼ˆä½œåºŸç ä»æ­£å¸¸è¿”回并记录扫码:作废码被扫是重要的防伪信号,不应记为查询失败)
            MesProTracePublicRespVO result = buildRespVO(rawCode, traceCode, entry);
            recordConsumerScan(rawCode, traceCode, entry.sourceType, entry.batchId, true);
            return result;
        } catch (RuntimeException ex) {
@@ -111,6 +125,15 @@
                    entry == null ? null : entry.batchId, false);
            throw ex;
        }
    }
    @Override
    public MesProTracePublicConfigRespVO getPublicConfig() {
        MesProTracePublicConfigRespVO respVO = new MesProTracePublicConfigRespVO();
        respVO.setOperatorName(MesProQrLabelBuilder.resolveOperatorName());
        respVO.setTraceUrl(MesProQrLabelBuilder.resolveTraceUrl());
        respVO.setImported(MesProQrLabelBuilder.isImported());
        return respVO;
    }
    /**
@@ -163,33 +186,65 @@
        // â‘  è¢‹ç 
        MesProBagCodeDO bag = bagCodeService.getBagCodeByCode(traceCode);
        if (bag != null) {
            return new TraceEntry("BAG", bag.getBatchId());
            // å·²ä½œåºŸçš„袋码(破损替换后作废)必须对外失效:国标要求二维码不得重复使用,
            // è‹¥ä½œåºŸç ä»è¿”回有效溯源信息,作废码即可被冒用销售
            if (MesProBagCodeStatusEnum.CANCELED.getStatus().equals(bag.getStatus())) {
                return new TraceEntry("BAG", bag.getBatchId(), bag.getItemId(),
                        MesTraceCodeStatusEnum.CANCELED.getStatus());
            }
            return TraceEntry.valid("BAG", bag.getBatchId(), bag.getItemId());
        }
        // â‘¡ æ‰˜ç›˜ç 
        MesProPalletDO pallet = palletService.getPalletByCode(traceCode);
        if (pallet != null) {
            return new TraceEntry("PALLET", pallet.getBatchId());
            // æœªå½•入批次的托盘取托盘内任一袋码品种,保证仍能展示品种信息
            Long itemId = pallet.getBatchId() == null
                    ? firstItemId(bagCodeService.getBagCodeListByPalletId(pallet.getId())) : null;
            if (MesProPalletStatusEnum.CANCELED.getStatus().equals(pallet.getStatus())) {
                return new TraceEntry("PALLET", pallet.getBatchId(), itemId,
                        MesTraceCodeStatusEnum.CANCELED.getStatus());
            }
            return TraceEntry.valid("PALLET", pallet.getBatchId(), itemId);
        }
        // â‘¢ æ‰¹æ¬¡ç ï¼ˆå«ä¸´æ—¶æ‰¹æ¬¡ç ï¼ŒgetBatchByCode å†…部兼容)
        MesProBatchDO batch = batchService.getBatchByCode(traceCode);
        if (batch != null) {
            return new TraceEntry("BATCH", batch.getId());
            if (MesProBatchStatusEnum.CANCELED.getStatus().equals(batch.getStatus())) {
                return new TraceEntry("BATCH", batch.getId(), null,
                        MesTraceCodeStatusEnum.CANCELED.getStatus());
            }
            return TraceEntry.valid("BATCH", batch.getId(), null);
        }
        return new TraceEntry("", null);
        return new TraceEntry("", null, null, MesTraceCodeStatusEnum.VALID.getStatus());
    }
    // ==================== èšåˆæž„建 ====================
    private MesProTracePublicRespVO buildRespVO(String requestCode, String traceCode, String sourceType, Long batchId) {
        MesProBatchDO batch = batchService.getBatch(batchId);
        if (batch == null) {
    private MesProTracePublicRespVO buildRespVO(String requestCode, String traceCode, TraceEntry entry) {
        String sourceType = entry.sourceType();
        Long batchId = entry.batchId();
        Long bagItemId = entry.bagItemId();
        MesProBatchDO batch = batchId == null ? null : batchService.getBatch(batchId);
        if (batchId != null && batch == null) {
            throw exception(MES_TRACE_CODE_NOT_FOUND);
        }
        MesProTracePublicRespVO resp = new MesProTracePublicRespVO();
        resp.setSourceType(sourceType);
        resp.setRequestCode(requestCode);
        // è¿½æº¯ç æœ‰æ•ˆæ€§ + æ ‡ç­¾å››è¦ç´ ä¸­çš„生产经营者与追溯网址,保证与标签印刷内容一致
        resp.setCodeStatus(entry.codeStatus());
        resp.setOperatorName(MesProQrLabelBuilder.resolveOperatorName());
        resp.setTraceUrl(MesProQrLabelBuilder.resolveTraceUrl());
        // å“ç§åŒºå—
        fillItem(resp, batch);
        fillItem(resp, batch, bagItemId);
        if (batch == null) {
            // è¢‹ç /托盘已生成但尚未录入生产批次:只返回品种信息,由前端提示未绑定批次
            resp.setBatchBound(false);
            fillAntiFake(resp, traceCode);
            fillTimelineAndDecision(resp);
            return resp;
        }
        resp.setBatchBound(true);
        // æ‰¹æ¬¡åŒºå—
        resp.setBatchCode(batch.getCode()).setTempCode(batch.getTempCode())
                .setBatchStatus(batch.getStatus()).setBatchStatusName(batchStatusName(batch.getStatus()))
@@ -212,18 +267,49 @@
        fillAntiFake(resp, traceCode);
        // å¯é€‰æ‰©å±•区块(有数据才返回)
        fillOptionalModules(resp, batch);
        fillTimelineAndDecision(resp);
        return resp;
    }
    private void fillItem(MesProTracePublicRespVO resp, MesProBatchDO batch) {
        if (batch.getItemId() == null) {
    /**
     * å“ç§å·²ä¸‹æ²‰åˆ°è¢‹ç æ•°æ®åŒ…:优先取扫码袋码品种,历史批次回退批次品种,批次/托盘入口取批内任一袋码品种
     */
    private void fillItem(MesProTracePublicRespVO resp, MesProBatchDO batch, Long bagItemId) {
        Long itemId = bagItemId != null ? bagItemId : (batch == null ? null : batch.getItemId());
        if (itemId == null && batch != null) {
            itemId = firstItemId(bagCodeService.getBagCodeListByBatchId(batch.getId()));
        }
        if (itemId == null) {
            return;
        }
        MesMdmItemDO item = mdmItemService.getItem(batch.getItemId());
        MesMdmItemDO item = mdmItemService.getItem(itemId);
        if (item == null) {
            return;
        }
        resp.setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification());
        resp.setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification())
                .setItemUnitMeasureName(unitMeasureName(item.getUnitMeasureId()));
    }
    /**
     * è¯»å–袋码列表中第一个非空品种编号
     */
    private Long firstItemId(List<MesProBagCodeDO> bags) {
        return CollUtil.isEmpty(bags) ? null : bags.stream().map(MesProBagCodeDO::getItemId)
                .filter(Objects::nonNull).findFirst().orElse(null);
    }
    /**
     * è¯»å–品种主计量单位名称,未配置单位或单位不存在时返回 null
     */
    private String unitMeasureName(Long unitMeasureId) {
        if (unitMeasureId == null) {
            return null;
        }
        CommonResult<MdmUnitMeasureRespDTO> result = unitMeasureApi.getUnitMeasure(unitMeasureId);
        if (result == null || !result.isSuccess() || result.getData() == null) {
            return null;
        }
        return StrUtil.trimToNull(result.getData().getName());
    }
    private List<MesProTracePublicRespVO.WorkerVO> buildWorkers(Long batchId) {
@@ -329,13 +415,57 @@
            resp.setPurchaseSources(CollUtil.isNotEmpty(purchaseSources) ? purchaseSources : List.of());
            resp.setSalesTargets(CollUtil.isNotEmpty(salesTargets) ? salesTargets : List.of());
            resp.setProcessTraces(CollUtil.isNotEmpty(processTraces) ? processTraces : List.of());
            List<MesWmBatchFlowRespVO> seedFlows = mesWmBatchService.getBatchFlowList(batch.getCode());
            resp.setSeedFlows(CollUtil.isNotEmpty(seedFlows) ? seedFlows : List.of());
        } catch (Exception ex) {
            // ä»“库批次体系(采购/销售/流转)未启用时容错,不影响核心追溯
            log.warn("[fillOptionalModules][批次 {} æ‰©å±•模块查询失败: {}]", batch.getCode(), ex.getMessage());
            resp.setPurchaseSources(List.of());
            resp.setSalesTargets(List.of());
            resp.setProcessTraces(List.of());
            resp.setSeedFlows(List.of());
        }
    }
    private void fillTimelineAndDecision(MesProTracePublicRespVO resp) {
        List<MesProTracePublicRespVO.TimelineVO> timeline = new ArrayList<>();
        if (resp.getProduceDate() != null) {
            timeline.add(timeline("生产", "BATCH_CREATED", "生产批次建立", resp.getProduceDate(), null,
                    resp.getBatchStatusName()));
        }
        if (resp.getProcessTraces() != null) {
            resp.getProcessTraces().forEach(item -> timeline.add(timeline("加工", item.getEventType(),
                    item.getEventType() == null ? "工序流转" : ("IN".equals(item.getEventType()) ? "工序投入" : "工序产出"),
                    item.getEventTime(), item.getQuantity(), item.getQualityStatus() == null ? null : String.valueOf(item.getQualityStatus()))));
        }
        if (resp.getInbound() != null) {
            timeline.add(timeline("仓储", "INBOUND", "仓储入库", resp.getInbound().getEventTime(),
                    resp.getInbound().getQuantity(), resp.getInbound().getStatus()));
        }
        if (resp.getSalesTargets() != null) {
            resp.getSalesTargets().forEach(item -> timeline.add(timeline("销售", "OUTBOUND", "销售出库",
                    item.getSalesDate(), item.getQuantity(), item.getQualityStatus() == null ? null : String.valueOf(item.getQualityStatus()))));
        }
        timeline.sort(Comparator.comparing(MesProTracePublicRespVO.TimelineVO::getEventTime,
                Comparator.nullsLast(Comparator.naturalOrder())));
        resp.setTimeline(timeline);
        MesProTracePublicRespVO.DecisionVO decision = new MesProTracePublicRespVO.DecisionVO();
        decision.setQualityRisk(resp.getQuality() != null && Integer.valueOf(2).equals(resp.getQuality().getResult()));
        decision.setInboundQuantity(resp.getInbound() == null ? null : resp.getInbound().getQuantity());
        decision.setSalesTargetCount(resp.getSalesTargets() == null ? 0 : resp.getSalesTargets().size());
        decision.setConsumerScanCount(resp.getAntiFake() == null ? 0L : resp.getAntiFake().getScanCount());
        decision.setRiskLevel(Boolean.TRUE.equals(decision.getQualityRisk()) ? "HIGH" : "NORMAL");
        resp.setDecision(decision);
    }
    private MesProTracePublicRespVO.TimelineVO timeline(String stage, String eventType, String eventName,
                                                         java.time.LocalDateTime eventTime, BigDecimal quantity,
                                                         String status) {
        MesProTracePublicRespVO.TimelineVO vo = new MesProTracePublicRespVO.TimelineVO();
        vo.setStage(stage).setEventType(eventType).setEventName(eventName).setEventTime(eventTime)
                .setQuantity(quantity).setStatus(status);
        return vo;
    }
    // ==================== åç§°æ˜ å°„ ====================
@@ -409,7 +539,20 @@
        };
    }
    private record TraceEntry(String sourceType, Long batchId) {
    /**
     * è¿½æº¯å…¥å£è§£æžç»“æžœ
     *
     * @param sourceType å…¥å£ç±»åž‹ï¼šBAG / PALLET / BATCH,未匹配到时为空串
     * @param batchId    ç”Ÿäº§æ‰¹æ¬¡ç¼–号
     * @param bagItemId  è¢‹ç ç»‘定的品种编号(未录批次时用于展示品种)
     * @param codeStatus è¿½æº¯ç æœ‰æ•ˆæ€§ï¼š{@link MesTraceCodeStatusEnum#VALID} / {@link MesTraceCodeStatusEnum#CANCELED}
     */
    private record TraceEntry(String sourceType, Long batchId, Long bagItemId, String codeStatus) {
        static TraceEntry valid(String sourceType, Long batchId, Long bagItemId) {
            return new TraceEntry(sourceType, batchId, bagItemId, MesTraceCodeStatusEnum.VALID.getStatus());
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.service.wm.batch;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
@@ -125,6 +126,14 @@
    List<MesWmBatchProcessTraceRespVO> getProcessTraceList(String code);
    /**
     * èŽ·å¾—æ‰¹æ¬¡å‡ºå…¥åº“æµæ°´ï¼ˆç§å­æµå‘ï¼šå…¥åº“ + é”€å”®å‡ºåº“等全部库存事务)
     *
     * @param code æ‰¹æ¬¡å·
     * @return å‡ºå…¥åº“流水列表
     */
    List<MesWmBatchFlowRespVO> getBatchFlowList(String code);
    /**
     * æ ¹æ® SN ç èŽ·å¾—æ‰¹æ¬¡
     *
     * @param sn SN ç 
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java
@@ -4,6 +4,7 @@
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
@@ -409,6 +410,11 @@
    }
    @Override
    public List<MesWmBatchFlowRespVO> getBatchFlowList(String code) {
        return batchMapper.selectBatchFlowList(code);
    }
    @Override
    public MesWmBatchDO getBatchBySn(String sn) {
        MesWmSnDO snDO = snMapper.selectOne(MesWmSnDO::getCode, sn);
        if (snDO == null || StrUtil.isBlank(snDO.getBatchCode())) {
@@ -423,13 +429,21 @@
        if (proBatch == null || StrUtil.isBlank(proBatch.getCode())) {
            return null;
        }
        // 1. å¤ç”¨å·²æœ‰ä»“库批次(同一生产批次号)
        MesWmBatchDO existing = batchMapper.selectByCode(proBatch.getCode());
        // 1. ä¼˜å…ˆæŒ‰ç”Ÿäº§æ‰¹æ¬¡ ID å¤ç”¨ï¼Œå…¼å®¹åŽ†å²æ•°æ®å†æŒ‰æ‰¹æ¬¡å·åŒ¹é…
        MesWmBatchDO existing = batchMapper.selectOne(MesWmBatchDO::getProBatchId, proBatch.getId());
        if (existing == null) {
            existing = batchMapper.selectByCode(proBatch.getCode());
        }
        if (existing != null) {
            if (existing.getProBatchId() == null && proBatch.getId() != null) {
                existing.setProBatchId(proBatch.getId());
                batchMapper.updateById(existing);
            }
            return existing;
        }
        // 2. æ–°å»ºä»“库批次:以生产批次号作为仓库批次号,质量状态置为待检验
        MesWmBatchDO batch = MesWmBatchDO.builder()
                .proBatchId(proBatch.getId())
                .code(proBatch.getCode())
                .itemId(proBatch.getItemId())
                .produceDate(proBatch.getProduceDate())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java
@@ -132,6 +132,18 @@
    List<MesWmMaterialStockDO> getMaterialStockListByItemAndBatch(Long itemId, Long batchId);
    /**
     * æŸ¥è¯¢ç‰©æ–™/批次的合格库库存记录(仅 stock_state=20)
     *
     * <p>同批次可能同时存在暂存库与合格库两条记录(种子质检按袋拆数量),
     * é”€å”®å‡ºåº“选库存必须从合格库记录中选,避免选中未质检的暂存库存。</p>
     *
     * @param itemId  ç‰©æ–™ç¼–号
     * @param batchId æ‰¹æ¬¡ç¼–号
     * @return åˆæ ¼åº“库存记录列表
     */
    List<MesWmMaterialStockDO> getQualifiedMaterialStockListByItemAndBatch(Long itemId, Long batchId);
    /**
     * æ›´æ–°åº“存数量
     *
     * @param id åº“存记录编号
@@ -165,16 +177,6 @@
     * @return ä»“库汇总列表
     */
    List<MesWmStockSummaryRespVO> getStockSummaryByWarehouse(MesWmMaterialStockListReqVO reqVO);
    /**
     * æŒ‰ä»“库批次更新库存状态(用于种子质检合格后,将暂存库整批转为合格库)
     *
     * @param batchId   ä»“库批次ID
     * @param toState   ç›®æ ‡åº“存状态
     * @param fromState åŽŸåº“å­˜çŠ¶æ€ï¼ˆé™å®šåªæ›´æ–°è¯¥çŠ¶æ€çš„è®°å½•ï¼‰
     * @return å½±å“è¡Œæ•°
     */
    int updateStockStateByBatchId(Long batchId, Integer toState, Integer fromState);
    /**
     * åº“位混放规则校验
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java
@@ -29,6 +29,9 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.materialstockbag.MesWmMaterialStockBagService;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import jakarta.annotation.Resource;
@@ -44,6 +47,7 @@
import java.util.function.Function;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -74,6 +78,11 @@
    @Resource
    @Lazy
    private MesWmBatchService batchService;
    @Resource
    private MesWmMaterialStockBagMapper materialStockBagMapper;
    @Resource
    private MesWmMaterialStockBagService materialStockBagService;
    @Override
    public MesWmMaterialStockDO getMaterialStock(Long id) {
@@ -121,9 +130,27 @@
            Assert.notNull(virtualWarehouse, "虚拟仓库(WIP_VIRTUAL_WAREHOUSE)不存在");
            virtualWarehouseId = virtualWarehouse.getId();
        }
        // 1.4 è§£æž warehouseType:转换为该类型仓库 warehouseIds(与 warehouseId / virtualFilter å¹¶å­˜ï¼Œä¿ç•™å…¼å®¹ï¼‰
        List<Long> warehouseIds = resolveWarehouseIdsByType(pageReqVO.getWarehouseType());
        if (warehouseIds != null && warehouseIds.isEmpty()) {
            return PageResult.empty();
        }
        // 2. åˆ†é¡µæŸ¥è¯¢
        return materialStockMapper.selectPage(pageReqVO, itemTypeIds, itemIds, virtualWarehouseId);
        return materialStockMapper.selectPage(pageReqVO, itemTypeIds, itemIds, virtualWarehouseId, warehouseIds);
    }
    /**
     * å°†ä»“库类型解析为仓库 id é›†åˆï¼ˆwarehouseType ä¸º null æ—¶ä¸åŠ è¿‡æ»¤ï¼Œè¿”å›ž null)
     *
     * @param warehouseType ä»“库类型,见 {@link cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum}
     * @return è¯¥ç±»åž‹ä»“库 id é›†åˆï¼›warehouseType ä¸º null è¿”回 null;该类型无仓库返回空集合
     */
    private List<Long> resolveWarehouseIdsByType(Integer warehouseType) {
        if (warehouseType == null) {
            return null;
        }
        return convertList(warehouseService.getWarehouseListByType(warehouseType), MesWmWarehouseDO::getId);
    }
    @Override
@@ -169,7 +196,11 @@
    @Override
    public List<MesWmMaterialStockDO> getMaterialStockList(MesWmMaterialStockListReqVO reqVO) {
        return materialStockMapper.selectList(reqVO);
        List<Long> warehouseIds = resolveWarehouseIdsByType(reqVO.getWarehouseType());
        if (warehouseIds != null && warehouseIds.isEmpty()) {
            return Collections.emptyList();
        }
        return materialStockMapper.selectList(reqVO, warehouseIds);
    }
    @Override
@@ -282,7 +313,17 @@
    @Override
    public MesWmMaterialStockDO getMaterialStockByCompositeKey(Long itemId, Long warehouseId, Long locationId, Long areaId,
                                                                Long batchId, Long vendorId) {
        return materialStockMapper.selectByCompositeKey(itemId, warehouseId, locationId, areaId, batchId, vendorId);
        // å‡ºåº“/占用只从合格库匹配,避免扣到同批次的暂存库(种子质检按袋拆数量后同批次双状态并存)
        return materialStockMapper.selectQualifiedByCompositeKey(itemId, warehouseId, locationId, areaId, batchId, vendorId);
    }
    @Override
    public List<MesWmMaterialStockDO> getQualifiedMaterialStockListByItemAndBatch(Long itemId, Long batchId) {
        List<Long> qualifiedWarehouseIds = warehouseService.getQualifiedWarehouseIds();
        if (CollUtil.isEmpty(qualifiedWarehouseIds)) {
            return Collections.emptyList();
        }
        return materialStockMapper.selectQualifiedListByItemAndBatch(itemId, batchId, qualifiedWarehouseIds);
    }
    @Override
@@ -302,11 +343,6 @@
    @Override
    public void updateMaterialStockState(Long id, Integer stockState) {
        materialStockMapper.updateById(new MesWmMaterialStockDO().setId(id).setStockState(stockState));
    }
    @Override
    public int updateStockStateByBatchId(Long batchId, Integer toState, Integer fromState) {
        return materialStockMapper.updateStockStateByBatchId(batchId, toState, fromState);
    }
    @Override
@@ -356,7 +392,11 @@
                || ObjUtil.notEqual(stock.getAreaId(), areaId)) {
            throw exception(WM_MATERIAL_STOCK_SELECTION_MISMATCH);
        }
        // 3. æ ¡éªŒåº“存数量充足(如果前端传了 quantity,则必须保证库存数量 >= quantity)
        // 3. æ ¡éªŒåº“存记录必须是合格库(暂存库存未质检,不可用于出库/占用)
        if (!MesWmStockStateEnum.QUALIFIED.getState().equals(stock.getStockState())) {
            throw exception(WM_MATERIAL_STOCK_NOT_QUALIFIED);
        }
        // 4. æ ¡éªŒåº“存数量充足(如果前端传了 quantity,则必须保证库存数量 >= quantity)
        if (quantity != null && stock.getQuantity() != null && stock.getQuantity().compareTo(quantity) < 0) {
            throw exception(WM_MATERIAL_STOCK_INSUFFICIENT);
        }
@@ -592,6 +632,32 @@
                .setRelatedTransactionId(outTransactionId)
                .setCheckFlag(false);
        wmTransactionService.createTransaction(inTransaction);
        // 9. æ•´ä½ç½®ç§»åº“时,逐袋迁移袋码明细位置(数量等于源库位活动袋数且为整数才视为整位移库)
        MesWmMaterialStockDO finalTargetStock = materialStockMapper.selectByCompositeKeyIgnoreVendor(
                sourceStock.getItemId(), reqVO.getToWarehouseId(), reqVO.getToLocationId(),
                reqVO.getToAreaId(), sourceStock.getBatchId());
        relocateBagsForMove(sourceStock, reqVO.getQuantity(), inTransaction, finalTargetStock);
    }
    /**
     * æ•´ä½ç§»åº“:将源库位的活动袋码明细迁移到目标库位。
     * <p>
     * ä»…当移库数量等于源库位活动袋数且为整数时执行(整位置搬移),将每袋明细的库存汇总、仓库、库区、库位
     * æ›´æ–°ä¸ºç›®æ ‡ä½ç½®ï¼›éƒ¨åˆ†ç§»åº“/无袋码明细的旧库存不做袋码迁移,保持汇总库存变动即可。
     */
    private void relocateBagsForMove(MesWmMaterialStockDO sourceStock, BigDecimal quantity,
                                     MesWmTransactionSaveReqDTO moveInReq, MesWmMaterialStockDO targetStock) {
        List<MesWmMaterialStockBagDO> bags = materialStockBagMapper.selectActiveListByItemAndBatch(
                sourceStock.getItemId(), sourceStock.getBatchId(), sourceStock.getStockState());
        if (CollUtil.isEmpty(bags) || targetStock == null || quantity == null
                || quantity.compareTo(BigDecimal.valueOf(bags.size())) != 0) {
            return;
        }
        for (MesWmMaterialStockBagDO bag : bags) {
            moveInReq.setBagCodeId(bag.getBagCodeId()).setBagCode(bag.getBagCode());
            materialStockBagService.moveIn(moveInReq, targetStock, null);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstockbag/MesWmMaterialStockBagService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.mes.service.wm.materialstockbag;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
/**
 * MES åº“存袋码明细 Service æŽ¥å£
 *
 * ç»Ÿä¸€å°è£…袋码库存明细的建立、迁移、出库、报废逻辑,供所有带袋码的库存变动入口复用。
 * æ¯ä¸ªè¢‹ç åœ¨åŒä¸€æ—¶é—´åªæœ‰ä¸€æ¡åœ¨åº“明细(唯一约束 bag_code),流转通过更新明细位置/状态实现。
 */
public interface MesWmMaterialStockBagService {
    /**
     * å…¥åº“:建立或激活活动袋码明细。
     * <p>
     * è¢‹ç é¦–次入库时新建明细;已出库/已报废的袋码再次入库时激活为在库并更新位置;已在库则视为重复入库抛出异常。
     *
     * @param reqDTO        äº‹åŠ¡è¯·æ±‚
     * @param stock         ç›®æ ‡åº“存汇总记录
     * @param transactionId æœ¬ç¬”事务 ID
     * @return è¢‹ç æ˜Žç»†
     */
    MesWmMaterialStockBagDO inbound(MesWmTransactionSaveReqDTO reqDTO, MesWmMaterialStockDO stock, Long transactionId);
    /**
     * å‡ºåº“:将指定袋码明细置为已出库。
     *
     * @param reqDTO        äº‹åŠ¡è¯·æ±‚ï¼ˆéœ€æºå¸¦ bagCode/bagCodeId)
     * @param transactionId æœ¬ç¬”事务 ID
     */
    void outbound(MesWmTransactionSaveReqDTO reqDTO, Long transactionId);
    /**
     * ç§»å…¥/迁移:更新活动袋码明细的位置与库存状态(移库、调拨、质检合格迁移)。
     *
     * @param reqDTO        äº‹åŠ¡è¯·æ±‚ï¼ˆéœ€æºå¸¦ bagCode)
     * @param targetStock   ç›®æ ‡åº“存汇总记录
     * @param transactionId æœ¬ç¬”事务 ID
     */
    void moveIn(MesWmTransactionSaveReqDTO reqDTO, MesWmMaterialStockDO targetStock, Long transactionId);
    /**
     * æŠ¥åºŸï¼šå°†æŒ‡å®šè¢‹ç æ˜Žç»†ç½®ä¸ºæŠ¥åºŸã€‚
     *
     * @param reqDTO        äº‹åŠ¡è¯·æ±‚ï¼ˆéœ€æºå¸¦ bagCode)
     * @param transactionId æœ¬ç¬”事务 ID
     */
    void scrap(MesWmTransactionSaveReqDTO reqDTO, Long transactionId);
    /**
     * æ ¹æ®åº“存汇总 ID æŸ¥è¯¢åœ¨åº“袋码明细。
     *
     * @param materialStockId åº“存汇总 ID
     * @return è¢‹ç æ˜Žç»†ï¼ˆæ— åˆ™ null)
     */
    MesWmMaterialStockBagDO getActiveByMaterialStockId(Long materialStockId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstockbag/MesWmMaterialStockBagServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,165 @@
package cn.iocoder.yudao.module.mes.service.wm.materialstockbag;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmMaterialStockBagStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * MES åº“存袋码明细 Service å®žçŽ°ç±»
 *
 * è¢‹ç æ˜Žç»†ç”Ÿå‘½å‘¨æœŸï¼šé¦–次入库建立(ACTIVE)→ ç§»åº“/质检迁移位置状态 â†’ å‡ºåº“/报废结束(OUT/SCRAPPED)。
 * å”¯ä¸€çº¦æŸ bag_code ä¿è¯æ¯ä¸ªè¢‹ç ä»…一条明细,复用同一行完成全生命周期流转。
 */
@Service
@Validated
public class MesWmMaterialStockBagServiceImpl implements MesWmMaterialStockBagService {
    @Resource
    private MesWmMaterialStockBagMapper materialStockBagMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesWmMaterialStockBagDO inbound(MesWmTransactionSaveReqDTO reqDTO, MesWmMaterialStockDO stock, Long transactionId) {
        String bagCode = reqDTO.getBagCode();
        if (StrUtil.isBlank(bagCode)) {
            throw exception(WM_MATERIAL_STOCK_BAG_REQUIRED);
        }
        // 1. æŸ¥è¯¢è¯¥è¢‹ç æ—¢æœ‰æ˜Žç»†ï¼šä¼˜å…ˆæŒ‰ bagCodeId,其次按 bagCode
        MesWmMaterialStockBagDO existing = materialStockBagMapper.selectByBagCode(bagCode);
        if (existing == null && reqDTO.getBagCodeId() != null) {
            existing = materialStockBagMapper.selectByBagCodeId(reqDTO.getBagCodeId());
        }
        if (existing != null) {
            // 2. å·²åœ¨åº“ â†’ é‡å¤å…¥åº“,禁止
            if (MesWmMaterialStockBagStatusEnum.ACTIVE.getStatus().equals(existing.getStatus())) {
                throw exception(WM_MATERIAL_STOCK_BAG_DUPLICATE, bagCode);
            }
            // 3. å·²å‡ºåº“/报废 â†’ é‡æ–°æ¿€æ´»å¹¶æ›´æ–°ä½ç½®
            return reactivate(existing, reqDTO, stock, transactionId);
        }
        // 4. é¦–次入库 â†’ æ–°å»ºæ˜Žç»†
        MesWmMaterialStockBagDO bag = MesWmMaterialStockBagDO.builder()
                .materialStockId(stock.getId())
                .bagCodeId(reqDTO.getBagCodeId())
                .bagCode(bagCode)
                .itemId(stock.getItemId())
                .batchId(stock.getBatchId())
                .batchCode(stock.getBatchCode())
                .vendorId(stock.getVendorId())
                .warehouseId(stock.getWarehouseId())
                .locationId(stock.getLocationId())
                .areaId(stock.getAreaId())
                .stockState(ObjUtil.defaultIfNull(reqDTO.getStockState(), stock.getStockState()))
                .status(MesWmMaterialStockBagStatusEnum.ACTIVE.getStatus())
                .quantity(BigDecimal.ONE)
                .frozen(false)
                .lastTransactionId(transactionId)
                .inboundTime(LocalDateTime.now())
                .build();
        materialStockBagMapper.insert(bag);
        return bag;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void outbound(MesWmTransactionSaveReqDTO reqDTO, Long transactionId) {
        MesWmMaterialStockBagDO bag = requireActiveBag(reqDTO);
        MesWmMaterialStockBagDO update = new MesWmMaterialStockBagDO();
        update.setId(bag.getId());
        update.setStatus(MesWmMaterialStockBagStatusEnum.OUT.getStatus());
        update.setStockState(MesWmStockStateEnum.OUT.getState());
        update.setLastTransactionId(transactionId);
        update.setOutboundTime(LocalDateTime.now());
        materialStockBagMapper.updateById(update);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void moveIn(MesWmTransactionSaveReqDTO reqDTO, MesWmMaterialStockDO targetStock, Long transactionId) {
        MesWmMaterialStockBagDO bag = requireActiveBag(reqDTO);
        MesWmMaterialStockBagDO update = new MesWmMaterialStockBagDO();
        update.setId(bag.getId());
        update.setMaterialStockId(targetStock.getId());
        update.setWarehouseId(targetStock.getWarehouseId());
        update.setLocationId(targetStock.getLocationId());
        update.setAreaId(targetStock.getAreaId());
        update.setStockState(ObjUtil.defaultIfNull(reqDTO.getStockState(), targetStock.getStockState()));
        update.setLastTransactionId(transactionId);
        materialStockBagMapper.updateById(update);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void scrap(MesWmTransactionSaveReqDTO reqDTO, Long transactionId) {
        MesWmMaterialStockBagDO bag = requireActiveBag(reqDTO);
        MesWmMaterialStockBagDO update = new MesWmMaterialStockBagDO();
        update.setId(bag.getId());
        update.setStatus(MesWmMaterialStockBagStatusEnum.SCRAPPED.getStatus());
        update.setStockState(MesWmStockStateEnum.OUT.getState());
        update.setQuantity(BigDecimal.ZERO);
        update.setLastTransactionId(transactionId);
        update.setOutboundTime(LocalDateTime.now());
        materialStockBagMapper.updateById(update);
    }
    @Override
    public MesWmMaterialStockBagDO getActiveByMaterialStockId(Long materialStockId) {
        return materialStockBagMapper.selectOneActiveByMaterialStockId(materialStockId);
    }
    /**
     * æŸ¥è¯¢æŒ‡å®šè¢‹ç çš„æ´»åŠ¨æ˜Žç»†ï¼Œä¸å­˜åœ¨æˆ–å·²å‡ºåº“/报废则抛异常。
     */
    private MesWmMaterialStockBagDO requireActiveBag(MesWmTransactionSaveReqDTO reqDTO) {
        MesWmMaterialStockBagDO bag = null;
        if (reqDTO.getBagCodeId() != null) {
            bag = materialStockBagMapper.selectActiveByBagCodeId(reqDTO.getBagCodeId());
        }
        if (bag == null && StrUtil.isNotBlank(reqDTO.getBagCode())) {
            bag = materialStockBagMapper.selectActiveByBagCode(reqDTO.getBagCode());
        }
        if (bag == null) {
            String code = StrUtil.blankToDefault(reqDTO.getBagCode(), String.valueOf(reqDTO.getBagCodeId()));
            throw exception(WM_MATERIAL_STOCK_BAG_NOT_ACTIVE, code);
        }
        return bag;
    }
    private MesWmMaterialStockBagDO reactivate(MesWmMaterialStockBagDO existing, MesWmTransactionSaveReqDTO reqDTO,
                                               MesWmMaterialStockDO stock, Long transactionId) {
        MesWmMaterialStockBagDO update = new MesWmMaterialStockBagDO();
        update.setId(existing.getId());
        update.setMaterialStockId(stock.getId());
        update.setItemId(stock.getItemId());
        update.setBatchId(stock.getBatchId());
        update.setBatchCode(stock.getBatchCode());
        update.setVendorId(stock.getVendorId());
        update.setWarehouseId(stock.getWarehouseId());
        update.setLocationId(stock.getLocationId());
        update.setAreaId(stock.getAreaId());
        update.setStockState(ObjUtil.defaultIfNull(reqDTO.getStockState(), stock.getStockState()));
        update.setStatus(MesWmMaterialStockBagStatusEnum.ACTIVE.getStatus());
        update.setQuantity(BigDecimal.ONE);
        update.setFrozen(false);
        update.setLastTransactionId(transactionId);
        update.setInboundTime(LocalDateTime.now());
        update.setOutboundTime(null);
        materialStockBagMapper.updateById(update);
        return existing;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -27,6 +27,7 @@
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductSalesStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmSalesNoticeStatusEnum;
@@ -43,8 +44,6 @@
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.enums.ErpSaleOrderOutStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
@@ -104,9 +103,6 @@
    @Resource
    private MesTraceIntegrityService integrityService;
    @Resource
    private ErpSaleOrderApi saleOrderApi;
    @Override
    public Long createProductSales(MesWmProductSalesSaveReqVO createReqVO) {
@@ -242,6 +238,15 @@
        if (pallet != null && pallet.getBatchId() == null) {
            throw exception(PRO_PALLET_NOT_BIND_BATCH);
        }
        // ç§å­è´¨æ£€æŒ‰è¢‹ç ç²’度流转后,出库只允许已质检合格的袋码/托盘
        if (pallet != null) {
            List<MesProBagCodeDO> palletBags = bagCodeService.getBagCodeListByPalletId(pallet.getId());
            if (palletBags.stream().anyMatch(b -> !MesProBagCodeQcStatusEnum.QUALIFIED.getStatus().equals(b.getQcStatus()))) {
                throw exception(WM_PRODUCT_SALES_SCAN_PALLET_NOT_ALL_QUALIFIED);
            }
        } else if (!MesProBagCodeQcStatusEnum.QUALIFIED.getStatus().equals(bag.getQcStatus())) {
            throw exception(WM_PRODUCT_SALES_SCAN_BAG_NOT_QUALIFIED);
        }
        Long codeId = pallet != null ? pallet.getId() : bag.getId();
        MesWmProductSalesDetailDO existed = pallet != null
                ? productSalesDetailMapper.selectBySalesIdAndPalletCodeId(reqVO.getSalesId(), codeId)
@@ -270,7 +275,8 @@
        if (quantity == null) {
            quantity = line.getQuantity();
        }
        List<MesWmMaterialStockDO> stocks = materialStockService.getMaterialStockListByItemAndBatch(batch.getItemId(), wmBatch.getId());
        // åªä»Žåˆæ ¼åº“记录中选库存(同批次暂存库记录不可出库)
        List<MesWmMaterialStockDO> stocks = materialStockService.getQualifiedMaterialStockListByItemAndBatch(batch.getItemId(), wmBatch.getId());
        BigDecimal outboundQuantity = quantity;
        MesWmMaterialStockDO stock = stocks.stream().filter(item -> item.getQuantity().compareTo(outboundQuantity) >= 0).findFirst().orElse(null);
        if (stock == null) {
@@ -338,12 +344,6 @@
        // æ‰§è¡Œæ‹£è´§ï¼ˆå¾…拣货 â†’ å¾…填写运单)
        productSalesMapper.updateById(new MesWmProductSalesDO()
                .setId(id).setStatus(MesWmProductSalesStatusEnum.SHIPPING.getStatus()));
        // å›žå†™ ERP é”€å”®è®¢å•出库状态为"部分出库"(表示已拣货/占用)
        if (sales.getSaleOrderId() != null) {
            saleOrderApi.updateOrderOutStatusTo(sales.getSaleOrderId(),
                    ErpSaleOrderOutStatusEnum.PART_OUT.getStatus());
        }
    }
    @Override
@@ -392,10 +392,7 @@
            updateSalesNoticeOutCount(sales);
        }
        // 6. å›žå†™ ERP é”€å”®è®¢å•的出库数量和状态
        updateErpSaleOrderOutCount(sales);
        // 7. å›žå†™è¢‹ç /托盘出库去向(供窜货稽查定位经销商地区),并生成出库完整性摘要
        // 6. å›žå†™è¢‹ç /托盘出库去向(供窜货稽查定位经销商地区),并生成出库完整性摘要
        List<MesWmProductSalesDetailDO> outDetails = productSalesDetailService.getProductSalesDetailListBySalesId(id);
        for (MesWmProductSalesDetailDO detail : outDetails) {
            if (detail.getBagCodeId() != null) {
@@ -425,43 +422,13 @@
        }
    }
    /**
     * å›žå†™ ERP é”€å”®è®¢å•的出库数量和状态
     */
    private void updateErpSaleOrderOutCount(MesWmProductSalesDO sales) {
        // èŽ·å–å‡ºåº“å•è¡Œ
        List<MesWmProductSalesLineDO> lines = productSalesLineService.getProductSalesLineListBySalesId(sales.getId());
        if (CollUtil.isEmpty(lines)) {
            return;
        }
        // ç”¨äºŽå­˜å‚¨é”€å”®è®¢å• ID,避免重复更新
        Long saleOrderId = null;
        // éåŽ†è¡Œï¼ŒæŒ‰ saleOrderItemId æ›´æ–°å‡ºåº“数量
        for (MesWmProductSalesLineDO line : lines) {
            if (line.getSaleOrderItemId() != null && line.getQuantity() != null) {
                saleOrderApi.updateOrderItemOutCount(line.getSaleOrderItemId(), line.getQuantity());
                // èŽ·å–é”€å”®è®¢å• ID(从第一个有效的明细项获取)
                if (saleOrderId == null) {
                    cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderItemRespDTO itemDTO =
                            saleOrderApi.getSaleOrderItem(line.getSaleOrderItemId()).getCheckedData();
                    if (itemDTO != null && itemDTO.getOrderId() != null) {
                        saleOrderId = itemDTO.getOrderId();
                    }
                }
            }
        }
        // æ›´æ–°é”€å”®è®¢å•的出库状态
        if (saleOrderId != null) {
            saleOrderApi.updateOrderOutStatus(saleOrderId);
        }
    }
    private void createTransactionList(MesWmProductSalesDO sales) {
        List<MesWmProductSalesDetailDO> details = productSalesDetailService.getProductSalesDetailListBySalesId(sales.getId());
        wmTransactionService.createTransactionList(convertList(details, detail -> new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId())
                .setQuantity(detail.getQuantity().negate()) // å‡ºåº“数量为负数
                .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode())
                .setBagCodeId(detail.getBagCodeId()) // è¢‹ç åž‹æ˜Žç»†å‡ºåº“时置袋码已出库;托盘型明细不逐袋处理
                .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                .setBizType(MesBizTypeConstants.WM_PRODUCT_SALES).setBizId(sales.getId())
                .setBizCode(sales.getCode()).setBizLineId(detail.getLineId())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeLineServiceImpl.java
@@ -7,16 +7,19 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.salesnotice.vo.line.MesWmSalesNoticeLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.salesnotice.vo.line.MesWmSalesNoticeLineSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.salesnotice.MesWmSalesNoticeLineMapper;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -38,6 +41,8 @@
    private MesWmSalesNoticeService salesNoticeService;
    @Resource
    private MesWmBatchService batchService;
    @Resource
    private MesWmMaterialStockService materialStockService;
    @Resource
    private MesMdmItemService mdmItemService;
@@ -118,12 +123,36 @@
    }
    private void validateLineSaveData(MesWmSalesNoticeLineSaveReqVO reqVO) {
        // æ ¡éªŒçˆ¶å•据存在且为草稿状态
        validateNoticeStatusDraft(reqVO.getNoticeId());
        // æ ¡éªŒç‰©æ–™å­˜åœ¨
        if (reqVO.getItemId() != null) {
            mdmItemService.validateItemExistsAndEnable(reqVO.getItemId());
        mdmItemService.validateItemExistsAndEnable(reqVO.getItemId());
        MesWmBatchDO batch = resolveBatch(reqVO);
        if (batch == null) {
            throw exception(WM_MATERIAL_STOCK_NOT_EXISTS);
        }
        if (ObjUtil.notEqual(batch.getItemId(), reqVO.getItemId())) {
            throw exception(WM_MATERIAL_STOCK_NOT_EXISTS);
        }
        BigDecimal qualifiedQuantity = materialStockService
                .getQualifiedMaterialStockListByItemAndBatch(reqVO.getItemId(), batch.getId()).stream()
                .map(MesWmMaterialStockDO::getQuantity)
                .filter(quantity -> quantity != null)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        if (qualifiedQuantity.compareTo(reqVO.getQuantity()) < 0) {
            throw exception(WM_MATERIAL_STOCK_INSUFFICIENT);
        }
        reqVO.setBatchId(batch.getId());
        reqVO.setBatchCode(batch.getCode());
    }
    private MesWmBatchDO resolveBatch(MesWmSalesNoticeLineSaveReqVO reqVO) {
        if (reqVO.getBatchId() != null) {
            return batchService.getBatch(reqVO.getBatchId());
        }
        if (StrUtil.isNotBlank(reqVO.getBatchCode())) {
            return batchService.getBatchByCode(reqVO.getBatchCode());
        }
        return null;
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeService.java
@@ -89,14 +89,6 @@
    }
    /**
     * ä»Žé”€å”®è®¢å•生成发货通知单
     *
     * @param saleOrderId é”€å”®è®¢å•ID
     * @return å‘货通知单ID
     */
    Long generateSalesNoticeFromSaleOrder(Long saleOrderId);
    /**
     * å®Œæˆå‘货通知单(出库完成后调用)
     * ç´¯åŠ å‡ºåº“æ•°é‡å¹¶è®¡ç®—å‡ºåº“çŠ¶æ€
     *
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java
@@ -18,10 +18,6 @@
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductSalesStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderItemRespDTO;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
import cn.iocoder.yudao.module.erp.api.sale.enums.ErpSaleOrderOutStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -53,8 +49,6 @@
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private ErpSaleOrderApi saleOrderApi;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
@@ -125,7 +119,7 @@
    @Transactional(rollbackFor = Exception.class)
    public void submitSalesNotice(Long id) {
        // 1.1 æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        MesWmSalesNoticeDO notice = validateSalesNoticeExistsAndDraft(id);
        validateSalesNoticeExistsAndDraft(id);
        // 1.2 æ£€æŸ¥æ˜¯å¦æœ‰è¡Œé¡¹ç›®
        List<MesWmSalesNoticeLineDO> lines = salesNoticeLineService.getSalesNoticeLineListByNoticeId(id);
        if (CollUtil.isEmpty(lines)) {
@@ -135,12 +129,6 @@
        // 2. æ›´æ–°çŠ¶æ€ï¼šè‰ç¨¿ â†’ å¾…出库
        salesNoticeMapper.updateById(new MesWmSalesNoticeDO()
                .setId(id).setStatus(APPROVED.getStatus()));
        // 3. å›žå†™ ERP é”€å”®è®¢å•出库状态为"已预留"
        if (notice.getSaleOrderId() != null) {
            saleOrderApi.updateOrderOutStatusTo(notice.getSaleOrderId(),
                    ErpSaleOrderOutStatusEnum.RESERVED.getStatus());
        }
    }
    @Override
@@ -188,54 +176,6 @@
        if (ObjUtil.notEqual(id, notice.getId())) {
            throw exception(WM_SALES_NOTICE_CODE_DUPLICATE);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long generateSalesNoticeFromSaleOrder(Long saleOrderId) {
        // 1. æ ¡éªŒæ˜¯å¦å·²ç”Ÿæˆè¿‡å‘货通知单(防止重复生成)
        MesWmSalesNoticeDO existNotice = salesNoticeMapper.selectBySaleOrderId(saleOrderId);
        if (existNotice != null) {
            throw exception(WM_SALES_NOTICE_ALREADY_EXISTS);
        }
        // 2. èŽ·å–é”€å”®è®¢å•ä¿¡æ¯
        ErpSaleOrderRespDTO saleOrder = saleOrderApi.getSaleOrder(saleOrderId).getCheckedData();
        if (saleOrder == null) {
            throw exception(WM_SALES_NOTICE_SALE_ORDER_NOT_EXISTS);
        }
        // 3. èŽ·å–é”€å”®è®¢å•æ˜Žç»†
        List<ErpSaleOrderItemRespDTO> saleOrderItems = saleOrderApi.getSaleOrderItemList(saleOrderId).getCheckedData();
        if (CollUtil.isEmpty(saleOrderItems)) {
            throw exception(WM_SALES_NOTICE_SALE_ORDER_ITEM_EMPTY);
        }
        // 4. ç”Ÿæˆå‘货通知单编码
        String code = autoCodeRecordService.generateAutoCode("SALESNOTICE_CODE", null);
        // 5. åˆ›å»ºå‘货通知单主表
        MesWmSalesNoticeDO notice = new MesWmSalesNoticeDO();
        notice.setCode(code);
        notice.setName("销售订单[" + saleOrder.getNo() + "]发货通知");
        notice.setClientId(saleOrder.getCustomerId());
        notice.setSaleOrderId(saleOrderId);
        notice.setSaleOrderCode(saleOrder.getNo());
        notice.setSalesDate(java.time.LocalDateTime.now());
        notice.setStatus(PREPARE.getStatus());
        salesNoticeMapper.insert(notice);
        // 6. éåŽ†æ˜Žç»†åˆ›å»ºå‘è´§é€šçŸ¥å•è¡Œ
        for (ErpSaleOrderItemRespDTO saleOrderItem : saleOrderItems) {
            MesWmSalesNoticeLineDO line = new MesWmSalesNoticeLineDO();
            line.setNoticeId(notice.getId());
            line.setSaleOrderItemId(saleOrderItem.getId());
            line.setItemId(saleOrderItem.getProductId());
            line.setQuantity(saleOrderItem.getCount());
            salesNoticeLineService.createSalesNoticeLine(line);
        }
        return notice.getId();
    }
    @Override
@@ -350,8 +290,6 @@
        productSales.setCode(code);
        productSales.setName("发货通知单[" + notice.getCode() + "]出库");
        productSales.setClientId(notice.getClientId());
        productSales.setSaleOrderId(notice.getSaleOrderId());
        productSales.setSalesOrderCode(notice.getSaleOrderCode());
        productSales.setNoticeId(noticeId);
        productSales.setSalesDate(notice.getSalesDate());
        // æ”¶è´§ä¿¡æ¯ä»Žå‘货通知单带入
@@ -365,7 +303,6 @@
        for (MesWmSalesNoticeLineDO noticeLine : noticeLines) {
            MesWmProductSalesLineDO salesLine = new MesWmProductSalesLineDO();
            salesLine.setSalesId(productSales.getId());
            salesLine.setSaleOrderItemId(noticeLine.getSaleOrderItemId());
            salesLine.setNoticeLineId(noticeLine.getId());
            salesLine.setItemId(noticeLine.getItemId());
            salesLine.setQuantity(noticeLine.getQuantity());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java
@@ -16,6 +16,7 @@
import cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction.MesWmTransactionMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
@@ -23,6 +24,7 @@
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import cn.iocoder.yudao.module.mes.service.wm.materialstockbag.MesWmMaterialStockBagService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -57,6 +59,9 @@
    private MesWmWarehouseLocationService locationService;
    @Resource
    private MesWmWarehouseAreaService areaService;
    @Resource
    private MesWmMaterialStockBagService materialStockBagService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -127,11 +132,20 @@
        materialStockService.updateMaterialStockQuantity(materialStock.getId(), reqDTO.getQuantity(), checkFlag);
        MesWmMaterialStockDO updatedStock = materialStockService.getMaterialStock(materialStock.getId());
        BigDecimal newQuantity = updatedStock.getQuantity();
        // åº“存状态:显式指定优先(如扫码入暂存库),否则按数量自动判定(≤0 å·²å‡ºåº“,>0 åˆæ ¼åº“)
        // åº“存状态:显式指定优先(如扫码入暂存库),否则自动判定——≤0 å·²å‡ºåº“ï¼›
        // >0 æ—¶ä»…目标仓库为合格库(20)才置合格态,否则保持暂存态(10),
        // é¿å…ç›˜ç‚¹/调整/调拨等操作把暂存仓库库存翻成合格态
        Integer stockState = reqDTO.getStockState();
        if (stockState == null) {
            stockState = newQuantity.signum() <= 0
                    ? MesWmStockStateEnum.OUT.getState() : MesWmStockStateEnum.QUALIFIED.getState();
            if (newQuantity.signum() <= 0) {
                stockState = MesWmStockStateEnum.OUT.getState();
            } else {
                MesWmWarehouseDO warehouse = warehouseService.getWarehouse(reqDTO.getWarehouseId());
                boolean qualifiedWarehouse = warehouse != null
                        && MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(warehouse.getType());
                stockState = qualifiedWarehouse
                        ? MesWmStockStateEnum.QUALIFIED.getState() : MesWmStockStateEnum.TEMP.getState();
            }
        }
        materialStockService.updateMaterialStockState(updatedStock.getId(), stockState);
@@ -139,14 +153,46 @@
        MesWmTransactionDO transaction = MesWmTransactionDO.builder()
                .type(reqDTO.getType()).quantity(reqDTO.getQuantity()).transactionTime(LocalDateTime.now())
                .itemId(reqDTO.getItemId()).batchId(reqDTO.getBatchId()).batchCode(reqDTO.getBatchCode())
                .bagCodeId(reqDTO.getBagCodeId()).bagCode(reqDTO.getBagCode())
                .warehouseId(reqDTO.getWarehouseId()).locationId(reqDTO.getLocationId()).areaId(reqDTO.getAreaId())
                .bizType(reqDTO.getBizType()).bizId(reqDTO.getBizId()).bizCode(reqDTO.getBizCode()).bizLineId(reqDTO.getBizLineId())
                .materialStockId(materialStock.getId()).relatedTransactionId(reqDTO.getRelatedTransactionId())
                .build();
        transactionMapper.insert(transaction);
        // 4. å¸¦è¢‹ç æ—¶ï¼ŒåŒæ­¥ç»´æŠ¤åº“存袋码明细(入库建立/激活、出库置已出库、移入迁移、报废)
        boolean skipBagManage = ObjUtil.defaultIfNull(reqDTO.getSkipBagManage(), false);
        if (!skipBagManage && (StrUtil.isNotBlank(reqDTO.getBagCode()) || reqDTO.getBagCodeId() != null)) {
            handleStockBag(reqDTO, materialStock, transaction.getId(), transactionType);
        }
        return transaction.getId();
    }
    /**
     * æŒ‰äº‹åŠ¡ç±»åž‹ç»´æŠ¤è¢‹ç æ˜Žç»†ï¼Œä¿è¯æ±‡æ€»åº“å­˜ã€è¢‹ç æ˜Žç»†ã€äº‹åŠ¡æµæ°´ä¸‰è€…ä¸€è‡´ã€‚
     */
    private void handleStockBag(MesWmTransactionSaveReqDTO reqDTO, MesWmMaterialStockDO materialStock,
                                Long transactionId, MesWmTransactionTypeEnum transactionType) {
        switch (transactionType) {
            case MOVE_IN:
                // ç§»å…¥/调拨/质检迁移:更新明细位置与状态
                materialStockBagService.moveIn(reqDTO, materialStock, transactionId);
                break;
            case IN:
            case ADJUST_IN:
                // å…¥åº“/盘盈:建立或激活明细
                materialStockBagService.inbound(reqDTO, materialStock, transactionId);
                break;
            case MOVE_OUT:
            case OUT:
            case ADJUST_OUT:
                // ç§»å‡º/出库/盘亏:置为已出库
                materialStockBagService.outbound(reqDTO, transactionId);
                break;
            default:
                break;
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createTransactionList(List<MesWmTransactionSaveReqDTO> reqDTOs) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java
@@ -69,6 +69,10 @@
     * æ‰¹æ¬¡å·
     */
    private String batchCode;
    /** è¢‹ç ID */
    private Long bagCodeId;
    /** è¢‹ç  */
    private String bagCode;
    // ===== åº“存位置 =====
@@ -128,4 +132,12 @@
     */
    private Integer stockState;
    /**
     * æ˜¯å¦è·³è¿‡è¢‹ç æ˜Žç»†è‡ªåŠ¨ç»´æŠ¤ï¼ˆé»˜è®¤ false)。
     * <p>
     * è´¨æ£€ç­‰éœ€è¦è‡ªè¡ŒæŽ§åˆ¶è¢‹ç ç”Ÿå‘½å‘¨æœŸï¼ˆå¦‚暂存出库与合格入库成对发生、袋码仅在最终落点迁移)的场景,
     * åœ¨å†™äº‹åŠ¡æ—¶ç½® true,避免 createTransaction æŒ‰å•笔事务类型自动置已出库/迁移,导致袋码状态错乱。
     */
    private Boolean skipBagManage;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warehouse/MesWmWarehouseService.java
@@ -90,4 +90,26 @@
     */
    MesWmWarehouseDO getWarehouseByCode(String code);
    /**
     * æŒ‰ä»“库类型获得仓库列表(按 id å‡åºï¼‰
     *
     * @param type ä»“库类型,见 {@link cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum}
     * @return ä»“库列表
     */
    List<MesWmWarehouseDO> getWarehouseListByType(Integer type);
    /**
     * èŽ·å¾—åˆæ ¼åº“ï¼ˆtype=QUALIFIED)的编号集合,用于按仓库类型过滤合格库存
     *
     * @return åˆæ ¼ä»“库 id é›†åˆ
     */
    List<Long> getQualifiedWarehouseIds();
    /**
     * èŽ·å¾—ç¬¬ä¸€ä¸ªåˆæ ¼åº“ï¼ˆtype=QUALIFIED,按 id å‡åºï¼‰
     *
     * @return åˆæ ¼ä»“库,未配置返回 null
     */
    MesWmWarehouseDO getFirstQualifiedWarehouse();
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warehouse/MesWmWarehouseServiceImpl.java
@@ -9,6 +9,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.warehouse.MesWmWarehouseMapper;
import cn.iocoder.yudao.module.mes.enums.wm.BarcodeBizTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.wm.barcode.MesWmBarcodeService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
@@ -22,6 +23,7 @@
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
@@ -175,6 +177,7 @@
        if (MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE.equals(code)) {
            MesWmWarehouseDO newWarehouse = MesWmWarehouseDO.builder()
                    .code(code).name("虚拟线边仓库").frozen(false)
                    .type(MesWmWarehouseTypeEnum.TEMP.getType())
                    .remark("系统自动初始化的虚拟线边仓库(用于生产报工与在制品管理解耦)")
                    .build();
            warehouseMapper.insert(newWarehouse);
@@ -183,4 +186,21 @@
        return null;
    }
    @Override
    public List<MesWmWarehouseDO> getWarehouseListByType(Integer type) {
        return warehouseMapper.selectListByType(type);
    }
    @Override
    public List<Long> getQualifiedWarehouseIds() {
        return convertList(getWarehouseListByType(MesWmWarehouseTypeEnum.QUALIFIED.getType()),
                MesWmWarehouseDO::getId);
    }
    @Override
    public MesWmWarehouseDO getFirstQualifiedWarehouse() {
        List<MesWmWarehouseDO> list = getWarehouseListByType(MesWmWarehouseTypeEnum.QUALIFIED.getType());
        return CollUtil.isNotEmpty(list) ? list.get(0) : null;
    }
}
yudao-module-mes/src/main/resources/mapper/home/MesBagCodeHomeStatisticsMapper.xml
@@ -62,6 +62,45 @@
        WHERE deleted = 0
    </select>
    <!-- ========== å‡ºåº“流通 / ä»“储 / è´¨é‡ ========== -->
    <!-- ç»Ÿè®¡å‘货通知单数(mes_wm_sales_notice) -->
    <select id="selectSalesNoticeCount" resultType="java.lang.Long">
        SELECT COUNT(*)
        FROM mes_wm_sales_notice
        WHERE deleted = 0
    </select>
    <!-- ç»Ÿè®¡é”€å”®å‡ºåº“单数(mes_wm_product_sales) -->
    <select id="selectProductSalesCount" resultType="java.lang.Long">
        SELECT COUNT(*)
        FROM mes_wm_product_sales
        WHERE deleted = 0
    </select>
    <!-- ç»Ÿè®¡é”€å”®é€€è´§å•数(mes_wm_return_sales) -->
    <select id="selectReturnSalesCount" resultType="java.lang.Long">
        SELECT COUNT(*)
        FROM mes_wm_return_sales
        WHERE deleted = 0
    </select>
    <!-- ç»Ÿè®¡æœªå¤„理库存预警数(mes_wm_stock_warning.status=10) -->
    <select id="selectStockWarningPendingCount" resultType="java.lang.Long">
        SELECT COUNT(*)
        FROM mes_wm_stock_warning
        WHERE deleted = 0
          AND status = 10
    </select>
    <!-- ç»Ÿè®¡å¾…评审不合格品单数(mes_qc_ncr.status=0) -->
    <select id="selectNcrPendingCount" resultType="java.lang.Long">
        SELECT COUNT(*)
        FROM mes_qc_ncr
        WHERE deleted = 0
          AND status = 0
    </select>
    <!-- ========== çŠ¶æ€åˆ†å¸ƒ ========== -->
    <!-- æŒ‰çŠ¶æ€ç»Ÿè®¡æ‰¹æ¬¡æ•°é‡ -->
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml
@@ -55,16 +55,50 @@
        WHERE rl.batch_code = #{batchCode} AND rl.deleted = 0
    </select>
    <!-- æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼šæ‰¹æ¬¡ â†’ é”€å”®å‡ºåº“行 â†’ é”€å”®å‡ºåº“单 â†’ OQC -->
    <!--
        æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼ˆç§å­æµå‘ï¼‰ï¼š
        â‘  è¡Œçº§åŒ¹é…ï¼šæ‰¹æ¬¡ â†’ é”€å”®å‡ºåº“行 â†’ é”€å”®å‡ºåº“单 â†’ OQC(行上直接记录批次的流程);
        â‘¡ æ˜Žç»†çº§åŒ¹é…ï¼šæ‰¹æ¬¡ â†’ æ‰«ç å‡ºåº“明细 â†’ é”€å”®å‡ºåº“单 â†’ OQC(种子袋码/托盘扫码出库时批次只写入明细表,按单据聚合数量)。
        åŒä¸€å‡ºåº“行若已有明细级匹配则不再重复出行级记录;排除已取消(状态 5)单据。
    -->
    <select id="selectSalesTargetList" parameterType="String"
            resultType="cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO">
        SELECT s.code AS sales_code, s.sales_date, s.sales_order_code, s.client_id,
        SELECT s.code AS sales_code, s.sales_date, s.sales_order_code, s.client_id, s.client_name,
               oqc.code AS oqc_code, oqc.check_result AS check_result,
               sl.batch_code, sl.item_id, sl.quantity, sl.quality_status
        FROM mes_wm_product_sales_line sl
        JOIN mes_wm_product_sales s ON s.id = sl.sales_id AND s.deleted = 0
        LEFT JOIN mes_qc_oqc oqc ON oqc.id = sl.oqc_id AND oqc.deleted = 0
        WHERE sl.batch_code = #{batchCode} AND sl.deleted = 0
        WHERE sl.batch_code = #{batchCode} AND sl.deleted = 0 AND s.status != 5
          AND NOT EXISTS (SELECT 1 FROM mes_wm_product_sales_detail d
                          WHERE d.line_id = sl.id AND d.batch_code = #{batchCode} AND d.deleted = 0)
        UNION ALL
        SELECT s.code AS sales_code, s.sales_date, s.sales_order_code, s.client_id, s.client_name,
               oqc.code AS oqc_code, oqc.check_result AS check_result,
               d.batch_code, d.item_id, SUM(d.quantity) AS quantity, MAX(sl.quality_status) AS quality_status
        FROM mes_wm_product_sales_detail d
        JOIN mes_wm_product_sales s ON s.id = d.sales_id AND s.deleted = 0
        JOIN mes_wm_product_sales_line sl ON sl.id = d.line_id AND sl.deleted = 0
        LEFT JOIN mes_qc_oqc oqc ON oqc.id = sl.oqc_id AND oqc.deleted = 0
        WHERE d.batch_code = #{batchCode} AND d.deleted = 0 AND s.status != 5
        GROUP BY s.id, s.code, s.sales_date, s.sales_order_code, s.client_id, s.client_name,
                 oqc.code, oqc.check_result, d.batch_code, d.item_id
        ORDER BY sales_date
    </select>
    <!-- æ‰¹æ¬¡å‡ºå…¥åº“流水(种子流向):库存事务 â†’ ä»“库/库区/库位 â†’ é”€å”®å‡ºåº“单(biz_type 119 å¸¦å‡ºå®¢æˆ·åç§°ï¼‰ -->
    <select id="selectBatchFlowList" parameterType="String"
            resultType="cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO">
        SELECT tx.type, tx.biz_type, tx.biz_code, tx.quantity, tx.transaction_time,
               w.name AS warehouse_name, loc.name AS location_name, area.name AS area_name,
               sales.client_name
        FROM mes_wm_transaction tx
        LEFT JOIN mes_wm_warehouse w ON w.id = tx.warehouse_id AND w.deleted = 0
        LEFT JOIN mes_wm_warehouse_location loc ON loc.id = tx.location_id AND loc.deleted = 0
        LEFT JOIN mes_wm_warehouse_area area ON area.id = tx.area_id AND area.deleted = 0
        LEFT JOIN mes_wm_product_sales sales ON tx.biz_type = 119 AND sales.id = tx.biz_id AND sales.deleted = 0
        WHERE tx.batch_code = #{batchCode} AND tx.deleted = 0
        ORDER BY tx.transaction_time
    </select>
    <!-- æ‰¹æ¬¡å·¥åºæµè½¬ï¼šäº§å‡º(OUT)/投入(IN) äº‹ä»¶èšåˆï¼Œæ— éœ€å»ºè¡¨ -->
yudao-server/src/main/java/cn/iocoder/yudao/server/framework/bizno/BizCodeSetInitializer.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
package cn.iocoder.yudao.server.framework.bizno;
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.List;
import java.util.Map;
/**
 * å·²ç”¨ä¸šåŠ¡ç¼–ç é›†åˆå…œåº•åˆå§‹åŒ–ã€‚
 *
 * <p>业务编码为「随机 16 ä½çº¯æ•°å­—」,唯一性靠 Redis é›†åˆï¼ˆ{@link BizNoUtils#USED_SET_KEY})查重。
 * è‹¥ Redis ä¸¢å¤±è¯¥é›†åˆï¼Œå·²ç”Ÿæˆçš„历史编码会被"忘记",之后可能被重新随机命中而重号。
 *
 * <p>因此:启动时若发现该集合 **不存在**,则从数据库中扫描所有业务编码列里的
 * ã€Œ16 ä½çº¯æ•°å­—」编码,重新载入集合,恢复防重能力。
 *
 * <p>说明:
 * <ul>
 *     <li>仅在集合缺失时扫描(正常启动无额外开销);</li>
 *     <li>best-effort,失败只告警、不影响启动;</li>
 *     <li>旧的带字母编码会被忽略。</li>
 * </ul>
 */
@Component
@Slf4j
public class BizCodeSetInitializer implements ApplicationRunner {
    /** é›†åˆåˆå§‹åŒ–标记(非 16 ä½ï¼Œä¸ä¼šä¸ŽçœŸå®žç¼–码冲突),用于在库内无编码时也标记为已初始化 */
    private static final String INIT_MARKER = "__initialized__";
    private final StringRedisTemplate stringRedisTemplate;
    private final JdbcTemplate jdbcTemplate;
    public BizCodeSetInitializer(StringRedisTemplate stringRedisTemplate, DataSource dataSource) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    @Override
    public void run(ApplicationArguments args) {
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(BizNoUtils.USED_SET_KEY))) {
            log.info("[run][已用编码集合兜底] é›†åˆå·²å­˜åœ¨ï¼Œè·³è¿‡åº“内扫描");
            return;
        }
        long start = System.currentTimeMillis();
        long total = 0L;
        try {
            String schema = jdbcTemplate.queryForObject("SELECT DATABASE()", String.class);
            List<Map<String, Object>> columns = jdbcTemplate.queryForList(
                    "SELECT table_name, column_name FROM information_schema.columns " +
                            "WHERE table_schema = ? AND data_type IN ('varchar', 'char') " +
                            "AND (column_name = 'no' OR column_name = 'code' OR column_name = 'temp_code' " +
                            "     OR column_name LIKE '%\\_no' OR column_name LIKE '%\\_code')",
                    schema);
            for (Map<String, Object> column : columns) {
                String table = String.valueOf(column.get("table_name"));
                String col = String.valueOf(column.get("column_name"));
                if (table.toLowerCase().startsWith("qrtz_") || table.toLowerCase().startsWith("act_")) {
                    continue;
                }
                List<String> codes = jdbcTemplate.queryForList(
                        "SELECT `" + col + "` FROM `" + table + "` WHERE `" + col + "` REGEXP '^[0-9]{"
                                + BizNoUtils.CODE_LENGTH + "}$'",
                        String.class);
                if (!codes.isEmpty()) {
                    stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, codes.toArray(new String[0]));
                    total += codes.size();
                }
            }
            // æ ‡è®°ä¸ºå·²åˆå§‹åŒ–,避免下次启动重复扫描
            stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, INIT_MARKER);
            log.info("[run][已用编码集合兜底] é›†åˆç¼ºå¤±ï¼Œå·²è½½å…¥åº“内 {} ä¸ªç¼–码", total);
        } catch (Exception e) {
            log.warn("[run][已用编码集合兜底] åˆå§‹åŒ–失败(不影响启动),请排查", e);
        } finally {
            log.info("[run][已用编码集合兜底] åº“内扫描耗时 {} ms", System.currentTimeMillis() - start);
        }
    }
}
yudao-server/src/main/resources/application-local.yaml
@@ -1,9 +1,12 @@
server:
  port: 48080
# H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀),需为公众扫码可访问的地址
# ç§å­æ ‡ç­¾ä¸Ž H5 æº¯æºé¡µé…ç½®
mes:
  trace-base-url: http://192.168.0.10:48080/trace.html
  # è¿½æº¯ç½‘址(二维码内容前缀),需为公众扫码可访问的地址
  trace-base-url: http://192.168.1.7:48080/trace.html
  # ç”Ÿäº§ç»è¥è€…名称(种子标签四要素之一,标签与溯源页统一取此值;未配置则无法导出标签)
  operator-name: æ–°ç–†å¡”里木河种业股份有限公司
--- #################### æ•°æ®åº“相关配置 ####################
spring:
yudao-server/src/main/resources/application-test.yaml
@@ -14,7 +14,7 @@
      - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Milvus,手动创建
      - org.dromara.trans.config.TransServiceConfig # TODO èŠ‹è‰¿ï¼šeasy-trans å…¼å®¹ Spring Boot 4 åŽç§»é™¤ï¼ˆTransServiceConfig å¼•用了已移除的 RestTemplateBuilder)
      # ç¦ç”¨æœªé…ç½® API Key çš„æ¨¡åž‹è‡ªåŠ¨é…ç½®ï¼ˆåªä¿ç•™é€šä¹‰åƒé—® Embedding çš„æ‰‹åŠ¨é…ç½®ï¼‰
      - org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration
yudao-server/src/main/resources/application-thzy.yaml
@@ -1,9 +1,12 @@
server:
  port: 48080
# H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀),需为公众扫码可访问的地址
# ç§å­æ ‡ç­¾ä¸Ž H5 æº¯æºé¡µé…ç½®
mes:
  trace-base-url: http://47.97.181.254:9001/trace.html
  # è¿½æº¯ç½‘址(二维码内容前缀),需为公众扫码可访问的地址
  trace-base-url: http://1.95.78.124:9008/prod/trace.html
  # ç”Ÿäº§ç»è¥è€…名称(种子标签四要素之一,标签与溯源页统一取此值)
  operator-name: æ–°ç–†å¡”里木河种业股份有限公司
--- #################### æ•°æ®åº“相关配置 ####################
spring:
@@ -55,11 +58,12 @@
        min-idle: 1 # æœ€å°è¿žæŽ¥æ± æ•°é‡
        max-active: 20 # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
        max-wait: 60000 # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´ï¼Œå•ä½ï¼šæ¯«ç§’ï¼ˆ1 åˆ†é’Ÿï¼‰
        time-between-eviction-runs-millis: 60000 # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 åˆ†é’Ÿï¼‰
        time-between-eviction-runs-millis: 30000 # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(30 ç§’,压缩空闲连接未被检测的缝隙)
        min-evictable-idle-time-millis: 600000 # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位:毫秒(10 åˆ†é’Ÿï¼‰
        max-evictable-idle-time-millis: 1800000 # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位:毫秒(30 åˆ†é’Ÿï¼‰
        validation-query: SELECT 1 FROM DUAL # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
        test-while-idle: true
        keepAlive: true # ç©ºé—²è¿žæŽ¥å®šæœŸå‘送检测包保活,避免被服务端 wait_timeout å›žæ”¶
        test-on-borrow: false
        test-on-return: false
        pool-prepared-statements: true # æ˜¯å¦å¼€å¯ PreparedStatement ç¼“å­˜
@@ -67,16 +71,16 @@
      primary: master
      datasource:
        master:
          url: jdbc:mysql://172.17.0.1:9000/mom-thzy?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          url: jdbc:mysql://172.17.0.1:9000/mom-thzynew?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          username: root
          password: thzy@123456..
          password: xinjiang@123456..
  # Redis é…ç½®ã€‚Redisson é»˜è®¤çš„配置足够使用,一般不需要进行调优
  data:
    redis:
      host: 172.17.0.1 # åœ°å€
      port: 6379 # ç«¯å£
      database: 1 # æ•°æ®åº“索引
      database: 3 # æ•°æ®åº“索引
#      password: dev # å¯†ç ï¼Œå»ºè®®ç”Ÿäº§çŽ¯å¢ƒå¼€å¯
--- #################### å®šæ—¶ä»»åŠ¡ç›¸å…³é…ç½® ####################
@@ -191,7 +195,7 @@
  storage: # æ–‡ä»¶ä¸Šä¼ é…ç½®
    path: /home/mom/javaWork/file # æ–‡ä»¶å­˜å‚¨æ ¹ç›®å½•
    url-prefix: /admin-api/system/storage-blob # URL路径前缀
    domain: http://47.97.181.254:9001 # åŸŸåå‰ç¼€ï¼ˆæŒ‰å®žé™…域名配置),前端配置好像不太对现在是直连后端,所有要后端端口
    domain: http://1.95.78.124:9008 # åŸŸåå‰ç¼€ï¼ˆæŒ‰å®žé™…域名配置),前端配置好像不太对现在是直连后端,所有要后端端口
    expired: 120 # token过期时间(分钟)
    use-limit: 10 # token使用次数上限
    jwt-secret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG # JWT密钥
yudao-server/src/main/resources/application.yaml
@@ -1,7 +1,17 @@
# H5 æº¯æºé¡µé¢åŸºç¡€åœ°å€ï¼ˆäºŒç»´ç å†…容前缀,形如 http://域名:端口/trace.html),
# éœ€ä¸ºå…¬ä¼—扫码可访问的地址;各 profile å¯è¦†ç›–。留空则二维码编码纯码值(兼容旧码)。
# ç§å­æ ‡ç­¾ä¸Ž H5 æº¯æºé¡µé…ç½®ï¼ˆå„ profile å¯è¦†ç›–)
# æ³¨æ„ï¼štrace-base-url ä¸Ž operator-name æ˜¯äºŒç»´ç æ ‡ç­¾å››è¦ç´ ä¸­çš„「追溯网址」「生产经营者名称」,
# éž local çŽ¯å¢ƒä¸‹ trace-base-url è‹¥ä¸ºå†…网/占位符地址将拒绝启动,避免印出消费者扫不开的标签。
mes:
  # è¿½æº¯ç½‘址(二维码内容前缀,形如 http://域名:端口/trace.html),需为公众扫码可访问的地址
  trace-base-url: http://localhost:48080/trace.html
  # ç”Ÿäº§ç»è¥è€…名称(标签四要素之一,标签与溯源页统一取此值;未配置则无法导出标签)
  operator-name: ''
  # è¿›å£å•†åç§°ï¼ˆè¿›å£ç§å­åœºæ™¯å¡«å†™ï¼›imported=true æ—¶æ ‡ç­¾ä¸Žæº¯æºé¡µå±•示进口商名称)
  importer-name: ''
  # æ˜¯å¦è¿›å£ç§å­åœºæ™¯
  imported: false
  # æ˜¯å¦å…è®¸äºŒç»´ç ç¼–码纯码值(不带溯源 URL),仅供本地兼容历史码,生产环境必须为 false
  allow-plain-qr-code: false
spring:
  application:
@@ -96,8 +106,10 @@
aj:
  captcha:
    jigsaw: classpath:images/jigsaw # æ»‘动验证,底图路径,不配置将使用默认图片;以 classpath: å¼€å¤´ï¼Œå– resource ç›®å½•下路径
    pic-click: classpath:images/pic-click # æ»‘动验证,底图路径,不配置将使用默认图片;以 classpath: å¼€å¤´ï¼Œå– resource ç›®å½•下路径
    # æ³¨æ„ï¼šå¿…须用 classpath*: å‰ç¼€ã€‚classpath: é€šé…æ‰«æä¸äº† jar å†…目录,
    # fat jar éƒ¨ç½²æ—¶åº•图在 BOOT-INF/lib/yudao-module-system-*.jar å†…会扫描为空,导致 /captcha/get æŠ¥ 6113
    jigsaw: classpath*:images/jigsaw # æ»‘动验证,底图路径,不配置将使用默认图片;以 classpath å¼€å¤´ï¼Œå– resource ç›®å½•下路径
    pic-click: classpath*:images/pic-click # æ–‡å­—点选,底图路径,不配置将使用默认图片;以 classpath å¼€å¤´ï¼Œå– resource ç›®å½•下路径
    cache-type: redis # ç¼“å­˜ local/redis...
    cache-number: 1000 # local ç¼“存的阈值,达到这个值,清除缓存
    timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行
yudao-server/src/main/resources/static/trace.html
@@ -4,130 +4,222 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="format-detection" content="telephone=no">
<title>产品溯源</title>
<title>新疆塔里木河种业股份有限公司 Â· äº§å“æº¯æº</title>
<style>
  :root {
    --primary: #2e7d32;
    --primary-light: #43a047;
    --primary-bg: #e8f5e9;
    --primary: #1f7a38;
    --primary-light: #35a055;
    --primary-bg: #eef7ef;
    --brand-blue: #0d5faa;
    --text: #1f2933;
    --text-strong: #111827;
    --text-sub: #6b7280;
    --border: #e5e7eb;
    --bg: #f5f7f4;
    --card-shadow: 0 1px 4px rgba(0,0,0,.06);
    --green: #2e7d32;
    --red: #d32f2f;
    --text-mute: #9aa1ab;
    --line: #e9ecef;
    --line-soft: #f1f3f5;
    --bg: #f6f7f6;
    --green: #1f7a38;
    --red: #c62828;
    --blue: #1565c0;
    --amber: #b06f00;
    --gray: #6b7280;
    --radius: 12px;
    --radius-sm: 8px;
    --shadow: 0 1px 2px rgba(16, 24, 40, .05);
    --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", monospace;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
  html, body { height: 100%; }
  body {
    font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", "Microsoft YaHei", sans-serif;
    background: var(--bg);
    color: var(--text);
    line-height: 1.5;
    font-size: 16px;
    line-height: 1.7;
    -webkit-font-smoothing: antialiased;
    text-size-adjust: 100%;
  }
  .wrap { max-width: 480px; margin: 0 auto; padding: 0 12px 32px; }
  /* é¡¶éƒ¨å“ç‰ŒåŒº */
  .brand {
  /* ========== é¡¶éƒ¨å“ç‰ŒåŒº ========== */
  .hero { background: #fff; box-shadow: var(--shadow); }
  .hero-top {
    max-width: 480px; margin: 0 auto;
    padding: 20px 16px 14px;
    text-align: center;
    display: flex; align-items: center; gap: 12px;
    padding: 12px 16px;
  }
  .brand .logo {
    display: inline-flex; align-items: center; gap: 8px;
    font-size: 20px; font-weight: 700; color: var(--primary);
  .hero-logo { flex: 0 0 auto; height: 44px; width: auto; }
  .hero-name {
    flex: 1; min-width: 0;
    font-size: clamp(16px, 4.6vw, 20px);
    font-weight: 800; line-height: 1.35;
    color: var(--brand-blue);
    letter-spacing: .3px;
  }
  .brand .logo .mark {
    width: 26px; height: 26px; border-radius: 50%;
    background: linear-gradient(135deg, var(--primary-light), var(--primary));
    position: relative;
  /* å®žæ™¯å¤§å›¾ä»¥ base64 å†…联在 background-image:后端安全过滤仅放行 html、css、js é™æ€èµ„源,
     ç‹¬ç«‹å›¾ç‰‡æ–‡ä»¶ä¼šè¢«æ‹¦æˆ 401 JSON;内联同时省掉 /prod/ çš„多文件部署 */
  .hero-banner {
    position: relative; width: 100%; padding-top: 40%; overflow: hidden;
    background-image: url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAUEBAQEAwUEBAQGBQUGCA0ICAcHCBALDAkNExAUExIQEhIUFx0ZFBYcFhISGiMaHB4fISEhFBkkJyQgJh0gISD/2wBDAQUGBggHCA8ICA8gFRIVICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICD/wAARCAGwBDgDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDKPWkz9PypzCm4r9vPw+w7NJn2FOCE9BmkKkdRSHYMmk59P0oooFYO3ak/z0p4BI6VNb2kt1IEjQsaTdtWNRu7Iq49h+VPCt6fpXWW3hwQQia6A/Gq39lzX94I7eLCg4yBWH1mGtnodLwk9NNWZ+n6cJpl81gAT0xXd2Oj20EYfK8DuKfp3hprILIY+g5LVkeIb37LlIbjae+DXm1KzxE+SDPUp4dYWHNNEPiCazghZYirSHjOK5Oz025vpv3cfyk8nFNSK61K5xHukJPWvR/Dnh+8ijQyoFGK6Z1Fhae+pzUqTxdW9tCDRPC6LEHaLJ9SK3JNFjEXlpCMdziugRo7aEJI4AHesfUfElrbqVQgkV4Pt61aeh9B7ChRhZlH7DBAgRYwSO+KhdGztQKo9SKwb/xRK7nYwjWsC51qeUn987fjxXpU8LVlrI8yrjKUdInaXGoWtlGRuV5Pwrlr/WvNYhpFz6AVhSTzzNjLHPYVIdNvAm+SFkU9zxXfTw0KesnqebUxU6itFaDpL9m6fyqm8hc81eh0uWRdzYUepOKq3EKwybFkD+4rqi43sjkkpbyK5/D8qT8KkWNnbABJq01skce6Xg+matysRy3RRH0H5UY+n5U84z8tFO5A0Zp4JpMGkpEjs/T8qTn2/KilpBYb/npSj8KUUHii4WD8P0pPyozS0gG04fQUYpRigVhc+w/KjPfA/KijmkMXdjsKTPPT9KDRQFhRz2H5UH6CijNIBPy/KjPsKDSUAH5U7OfT8qbj2ooEOzj/APVRn6flRmjvQA4f54pwP0/KmjrTqQxcjPQUflSYpwJHUZ+tINBuxmPyrn8KeLK6YZW3Yj1C0C5mQ/I20e1Tx6hdqeJ2/Ok3LoNKHUiTTLtzxD+Yp50q9UZMBI9hWtb6zOijeiyfWtW18R2m4JPCBmuadWqtonXCjQlvKxz1hZPuLSJhR146VLdRqpLIAPeujv7yyeHMRVVPpXLXCozEpIW+tTCcpvmegVqUaa5Yu5SkkZsgnNRA4PQflVudIlhXb9/vVVVya6k9DicbMmD8AbQKQlc8/wAqRgB3ppxQBMhgIwwwfYUwxliSikr64poYocgAVb+2O0W3g+xpO62KVupClpLIRkbR6kVowQW9qN7kMfpVAXUv3S2FHYVMssB/1jZzUy5mOPKW7i9kmG23Un2ArNkuLlWIdiD6EVqxXUdqm6IBs1Ru71bg5aNRUw3sloVPa7epTaeY9XqMkseacSv92m5rYxJ45njTClQPpULvuJP9KbSc0DvcD9B+VAPsPyoApaYxPfH6UZ+n5UvelxSuFhufb9KPy/KlIoAphZDfy/KgevH5U4igCi4rB+H6UY9h+VLilouOw3/PSlB4/wDrUYoHtQA4H2H5U4fh+VNFLSGh+fp+VOH+eKYKcDSKHj8Pypcew/Kmg07NTqOwYHoPyoxnsPypc0oouOwzZz2/Kl2n0H5VIMdaeAKXMVykO0jsPyo59B+VTbaNtLmHykPQ9B+VOz9PyqTZR5YouLlGZPt+VLn6flTvLo8sii4+UM8UD8Pyp20gdKKVx2D8vypw/D8qSnLSuUPB46D8qPwH5UtAqR2Afh+VPHTt+VNpwpXHYXPsPyo/AflSgUoFK47DR9B+VSL+H5UgFOFK5VhR16D8qd+A/KkFPFTcaQD8PypwHsPypVFShahs0SGgH2/Kngew/KnBaeEqOYtRGY9h+VJgg9P0qcJQ0fFLmK5SEDnoPypcYHQflTgvNLtOaLhykZ+g/KjaSeg/KpCtOVOM0cwcpFtycAD8qlWI4yQPyqeGAtzip3j2rjFZufQ0VPS5nuO2B+VR7MHoPyq20eOtMKe1UpEOJWI9h+VNI56D8qsFfSm7KtSIcSLHHQflSEH0H5VNso207k8pW289vypCv0/KrBWoyPSqUiXEh28ngdPSipMc/nRVXIsclb20lzMI4x17+ldPbeE4RCJ7i8GP7oFd3pXhDTreJSgHmYGcnNS33h+JULed+Gawq5nGUuWDserSyuUI801c85udMgt0doPmwK5uQEucgivQb9I7SJggDEd2rk47KXUL4KI2KseSi134erdNyPNxNHlkox3MmKB5ZAkalmPYDNaVzoN7aWouJomVSM8jFeoaB4RtLa3WXyD5g53N1qn4ntr5oHWK0aVQMZHNcv8AaUZ1VCGx2f2XKFJznv5HlEY3ShOeTXdaHaLbxArEAx7msePw7qnliaK33H+6Ooq9awa8rLbwwMjdywrfEVI1I2jJHPhqcqUrzi/uOqTT3vZFSXpXTafoUFtHlEAOOpqDw5pk8MSvet8/fNdYUt2j2+YM/WvlMViWpckXofX4bDrl52tTitZW5ihZElGK84n0C91G8LtvYE+lev3mmW8k+Tlz2HarEGn+UnzIAPpXRRx3sI+7uc9fAqvL39jk9B8N2llCjSQjeo7itDUtShs4iqEDHatS8lEMbLGmT7VyU+i3upXBYAgHsaIS9tLnqvQKkXSjyUlqc3q2uX05KwnAPAxXPix1i+kIBdh3wDxXpUPg9IHE13Oq47U+fUdB0uMoJ1Z167ea9SGLhFctCNzyamDnJ81eVkeWT6Hd2x/fOuT2J5po0iUgMs0RP90nBNdFrniWyuji1tdxH8bDGa5m51C4ufvkDHTFerSlWnFOSseLWjRhJqLuXEj1CxXcsMaAfxDB/WoJNWuJBtcAn1rPZ5GGGdiPQmmc1sqaesjndR7RLUt5NKm0t8o7Cq6rk5JpMUAE1oklsQ5N7knm7RhBio3ZmPJzTwjHtSFNvFJWE22iLFOxS7TSgVVyRhFJtqQikIpXAaBS4pQtGPSkMKQ9aXFGKBXG4pcUuKUCgVxMUlPxSYpBcSlzRigA9qYXEoxS4paQXG0U7FGMUBcbijFOxRikA00CnEUAUBcQCnYzS0UgEApRRmlyKQXHKcGpM+ZwSB71DRzRYRK0QUZ3qfxpol2rhUXPrjNRGii3cPQNxBzmnbiec803bQBQBZinUDbKCwprSgPmPIHvUJopWQXHu29smk6DjimiloAWjmkpM0hjvrTsD1qOnA0wHbR60w8UvWgigQokcLt3HHpTevWkNJQG46iijFACGgUp9qBQMMUmKdiigBMUGlxRigBtAp2KMc0ANpaUikwaACil20oFAxMUoApccUAUAGBSgc9aAKXFIYUoFJSigEOApcGig0ihRS0gpRSKQ4GnBqZg0oFIokDYp4aou1HNKw7lgOKeChqrzTgTUtFKRbCqe+Kd5QPQ1VDkVKsnvUNMtNE/kH600wN/dpyS471Os471F2ikospmH2xQIj2q+JYm6qKeEgYdcUudlezTM7Yw7UmMHpWn9mQ/damm0bsM0e0QezZn9qB1q21qw/hIqNrdhT5kLlaIx9aeKPKcdqUKw7UMEO2g9KTaRVq3t3l5PC+taC2USrhlJrF1EjVU3LUxRUi1oSaehP7s4+tV3tJo+qEj1FNTTE6bRGDUyEHrUQB79akUGkxxLKpmpUTsajiOMZrQijD9BmsJOx0xVyuIiD0qTyMjGK0o7QtxjmpxZHbnbXO6qOpUWzBMBDYxS+TitmSzIbOKabXOOKftUL2DMgwn0q1FYO4ACmtay0xp7hVC5FdRDorDB28Cueriow0Oqjg5T1scrHYCOLLDmqU0QUk11moRLAh3DpXKzszuTjipo1HPUK9NU/dKLpznFQMuKuOH6BcmmrayMcsMD0rtUktzglFt6FQRlugpfKx1rR+z7RyKhZR0FPnuLktuUWSoyKsyYFV2YDpWqMXoRkVExpzN3qNmrVIybEzg0VGW5P0orSxiehatqNrYSMfNCqBxzXIXXjFAzJEjP6Hsa7xfAjXkgubyR5AwBEZONtSr8PNJhRibYKT3znFeDSxWEpq0ndn1lXC4yprC0UcHpt7JqJzPah1NdXYWllEyuq4PoOgqK60u20lhskBTpjGCKijuIz904FbVZqqrw0RnSh7J2qas6uO9jUBcAgdsVFcMlzjfCCOwPSs20uYMda1orqzwORn615UocjukepGpzqzZkXck+PJhtxGv94DFVoUWD5nfc3qal1zWre1jYBgprzbU/EV1IxWB9qHuK9PDYapWjtZHmYnFU6Du3dnos+twW4/eXCrjsDWefEdrLIFW7CfU15ojX9++2JJJmPXHNSvZ3GmSLLdqpPUIGya9BZfTWjep5kszqPVR0PZrC9haIPHLu9zV9tQhYbDKufTNeIzeLNQMXk2+IU9utZx1rUSc/aXH0Nc/9jyk7t2Nv7bhHRK576sdqw3tIPzol1TT7OIs88YCj1FeEf8ACRasYvL+1sq+x5qpPfXNx/rJWI9M045NJv356ClnkUvchqemav4y024ZreOYbe79j+NcbqGqaSEcWVr5kzdZGPA+grnCxNAr1qOCp0VaNzx6+Pq1tZWByWYsepphHFPxRiu+55zI9tG2pdtJtpXER4pRmn4pQtFxjcsRSbTUoWl20XFYh2mjbU2wntS+We9K4FfbS7an8pj2NN24OD1ouKzIttJtNW1t5HPyoTT3spkGWTFLmQ+WXYobeaXbVpYGJ+6anFmpH8RP0pc6BQbM3HtS4q40SI2D2qFiDwq073FYiVGY4A5pzRsvDCp4UbdmnyRvJJhQWNK+ocuhDFbCU/NIqD3pJIkVtqSB8dxWlBpBI33VysCe55qZpNJslKwIZ3/vtWftNdNTZU7L3tDE8tvQ03aR2rRmv94wsYAqkXJPQCtU29zBpLZke00beadnPeimIbikxTzSEUhiYoxS0UCG4opaKADBpBS5o70AGaQ0vFFIBMUtLRSGJRS4oNAhMUlLRigA70oFFLQAlJTj1pMUDExS0UopAN5paXHFGKAExmjbTsYooAbik5p1KBTAbzQKfgUYx2oENwaXmnYoxSGN5owafikxQA3mkwc06lxTAZg0YNPxS4oGMxS4p2KT8aQCUoBo/CloGJg0uDTgKXFIBnNOApcUAUDClxS4o5pFIAKUCgDNPAoGJijBp4FGKkYntSgUoFOAouUIBS7acBTgtSNDMU4CnYpQKQxKUE0oWnbaQxAzZp4c0gXilC1JSZMsxHep0uWHeqmKcBUOKZopNGkl2P4gDVlZLd/vLWMAR61IHYd6ydNdDVVX1NjyLZ+jYpDp6t91gazFmcdDUqXbr3NQ4SWzNFUg90XQJbY7cZFTLdRMcONpqot8xHPNSJIsw/1YrNxfVGimujNCNI5OUYGrAtpMfdyKz4bVmbdHlfcGtyxjuUIDsGX3Nc1R8uzOunHm3RSbTYpuGj2n1FVpNCuFG6Ebx6d67KKBJMblArSt9NQ8qwrjeNcDsWA5zzL7NJE+10Kkeoq7bhlIOK9NOgQ3K4liDe+KoXHg0j5rUn/dNT/aNOWktCv7Mqx1jqc9ZgOQDxW9DYiRQQKqro91aviSFhjviuh0yJuFYVyV6ytzRZ3Yeg78skY02lnH3agXTScDbXfjTlkTkClg0mNJAzgYB6VwrH2R6P1C7MjRtCEUYkdME1rXcEdvbnHpWqAAMKMCq1xbGfgnivNlXlUnzSZ6UaEYQ5Yo861K2nu5cIv0FQW/h6UHfcRZHpXpMWnW0K52An1NU76WGJCAor0YY+VuSCPNngIp882eez6SsZOI9tUZYEirodSvc5CiuWupWcmvWoynP4jxMQoRfulO4kVc1myTZJxVmZWY1WNux7GvVppLc8epd7FV2JqBs1eMDD+Go3hP0roUkcziygQe9NIq20Q7kVGY0/vCtVIyaKu0Z/CirG2IZ57GiquRY9ym1B4YchhnHas5dQvZWLb/AJR7cUxURiC7b+BxTpJVCEAAEdAK+MjCK0sfoDnJ63MvV5opV3PECw7+tcXePmUlE8sD0NdtPJEyMZQPxrAm021utzCUgHtXrYaSgrM8rFJzehy8upXEZwjFqtaXd6reXAQAeXnlumKW+s7SwDOvzEf3jWZbavdCTbEoVexAr11FTheCPFdR05rnkdNrsFsbBi0D3EwXhVrg7PS7vULsKIGjQH+NcD6V0t14pazt9qqkkxHcZrnJPEeoM7Mjqhb0FaYanWjFpL5mOLq0JzUm/kjtYrWy0bTmZ5IomI5x615vqNwLm9kkViVJ4yaLm9ubli0szuT6mqfNdmHw7ptyk7tnDicSqqUYqyQnNJmlP1oFdhwhThSAc0oFAC4oFLRipAOtKBQBT1FACAUbealC56CnLGx7VNykiDZmnrHzVpYCeoqxHbZqHMtQbKQiPSpFtnPJU49a2rSwG4FgPxpL/ehEUYx9Kx9rd2Rv7G0eZmOyRoMA80ioT9xC34VsWmjPOPMZSW9K0E0e7KgIgHsODUOvGOly44actbHNfZLmX+EgVds9GZzvkBwK6200Xy8NOfwrRNvCE2KAo9a5Z43pE7qeX/akcqLNY1wBkCmJpE9y+4qQnqa6T7JbRtksGP1pLiXZHsiXJ9BWPt5X903eGj9o5i7tYrNCMrn2rIB8wnBIrpJNKu72XdKCq+9K2j2tvH+8mC47Z5rqjWjFWbuzinh5Sd0rI52HTvPcjPHcmraaEXb92C30q3Ld2dopWIhj71mXGt3LqViYovtxWqdWfwmMlRpr3tWXZdMt7VM3E8ceP4Qcmsy4vYk+S2HH97GKpPPJISWOSe55qLqea3hTa+J3OWdZP4FYc8zvyxyfeojnPNPC9qXbW+iObciNJiptlG2i4rEO00uKl200igBtFLinY9qAGY4puOakxSYpAxm2jFPxS0XAjxSYqXHFNIoAjpcZp2KUCgBoFLinY9qXAJpAMGM0U7bSEc0ANxmjHvTsUuBQA2l4pcCkwKAEwKTtTsUYoAaBTgKBTqQCUmKdS4oAYBSmnUYoCwzFLinYp2BQMYBTsUoHtS0DsNIpMc0/FGO9A7DcUmKdil20CsMxS4p4Sgr6UirDMCjFO20uKYWGEUgAp+KAKB2GgUAU8LTsUgsMxSjmnYpQKQ7DcUuMU8CgigdhtLgUuCaXbSHYbingUoUU4CkOwmKMU/FLtpDsNAp4WnBacFqblJDAKdt5p2BSjFIY0ClxTsUoFIdhoApwAp22nBaTZVhuPelA5p2KNtK47CgelOAFAWn4FSxpDcUuKXFKF96kqw3bxShamWLcOtPFsfWpukUosgx9at27Y4NOS0J71ZisCSPmxWcpxsaRhK+hYhk2gVdjuSMYDUltpkrMOc1vWekEsPNIC151WpCO56dGlUkZsd7NFjO4Vft9YkQjLGt+TSNOniWPIAA61EvhTTzg/bSvtXnPEUWveR6kcNXi/cdwtNflUja/510ljrxkAWWMHNZ1p4X0tOWu8/jWzbaRpUJH77f9WrysROhLZfgevhoYiPxNfeakU1vcKMqPxFOFrbhtyxgH2pIktEGI9v51Y47GvJbt8J7KV1qAGB0paKKgsKSlxSE4Gc0ANdS64BxWbc6UZ8kyjHvT7rU1twcLmudvfELnIGRXbQo1XrE4a9alFWmSXeg2oz5t0PoKwrvSdOhTd5m78aq3etuxPU1iz6qWJzmvdoYet1kfPV8TQ6RLM6WiZ2R1lzyhc7FAqKXUN1UnuQa9enSa3PEq1k9hs9xJzzj6VQZ5W/iNTvICc1CTmu6Kt0PPk2+pCVc9TSeW3rUhzTNxrVGOg3yj69qKcGOfwoqrsk75fEUFvF+8kQkgYA61TTxNbNOTIvynvXESA5yWqMyhRiuJYCmes8xq6HX3HiVWLCOLcOxasafWn+YIQhPpWG8ztwOKhbOOa64YWEehxVMbUl1LM900pJllZ/Y1QlunUEIcCkcGoSjE12xgkcEpyZDIzOck5qMirgtpG/hNL9kk/umteZIx5WyjtNIVNX/sjHoDT10+Z/up+dHOh8kuxl7Tmjaa05LJ4hlsVAYD6UKaZLi1uU9vtTgOelW1tmJ4FWU02UjJAA96HNLcFCT2M3ZmnCJj2rT+yxR53SDI9KAUBGxcn3qfadivZ23KP2ZwNxHFIAF61ekEjA5GBVfyJGOFUmhSvuDjbYRGXPIq3EUPYVC1nNGu5lNNAdecEVLs9hpuO6NWMR8FhTZJURs7vwFZ4mkWr+nWovXOdzH0xWMoqOrOiMnP3Y7kQ1N0f5Rx703+0XaTcQDV+90OWBd8iHafQdKy2RIRhEyfU0R9nJXiE/awdpGxZ397Iw8pcKK211WaOP8Aebc+lctZS3hPlQoWz6Cuu03w/LKgluuCa4sQqcNZHo4V1KitC5ny6zdO21Bj2pq3V5MduHz6AV1Uem6VAcPgkd2NSy3um2q4ijBI9BXH7eO0IHesPPecznI7LUpACsBUernFMnN1aKWd4+PQU7VddujkQggViRah5pIu13+9dUKc5LmkkcdStTi+WLYt34gusFFXHvWHNd3MxJeVufetu4gSVc29rkn8aof2Rfuci3Kj3OK7qTpxW1jzK/tpu12zJIJ5JJpNtbLaHdquTsz6Zqs2nzof3ibfc1uqsXszkdGa3Rn7eKAnoKuNHGnG7JqM47VXNcjltuQhOad5fGakyOmKTJ6ZouxWRERTcVNhfrRhfSncViHFBWpSB24pNtFwsQlaMelSkUYzTuIix7UYqXFJg0rgRbaXbUmKTFFwI8UhFSY9qTFO4Ee2jBp+2jbSuAmKXFOC0YoAaV4pu32qSkxQAzHPSjHtT8UbTQMYBRipNtG2i4Ee3mjFPxQFouAzFOC+1PC04ClcZFtox7VLto20XAj2mjbT9po2mgLDMClFOxShaLjsNpce1SbcUbaVyrDAPajb7VIBS4ouOxFt9qNtTbDRsouOxHto21Jtpdp9KLjsRbaTaKn2UbaLhYg20bKm20m2i47EYXFLtqXbxRtpBYi204LUmylCUrjsR7aXb2qTbQEpXHYj204JUm00oQ0rjsRhadtqUKadtpXHYiC07bUgWnBaVyrEYXmlxxUm2l20rhYi25owc1LtpNvNK47DQvFO2804LT8UrjsRgU4D2p+2lC1LGhuKUCnbaXFK4xAKUClAoxzSAPwpR9KXFOC0hiqSOasxzAdRVfBpwFS0mWpNbF5bpB/DUy6gidEJrNGacATxWTpx6miqyRsJrciH5IgBV2HXLiQbVIU1zu0jqKcjlGDA4IrKVCD6G0cRUXU6yG9uCeZDWvFJ5tszNIVZR69a4yLU2AG5B+FTi/uJzsiBxXDPDN+R3U8Ul1udFHdzo5AlJH1rSt72RmAVmY+grB0/TriZg08hUegrtdMt7a1UbUBPqa8/EOEF3Z6mFU5u70RpabaXcyh3JjX3rfRBEmC3Tuawptchs48lhn0rEm1+4vHIViqV4roVazvayPeWIpUVa92dqbmIHAYE+1SoyuuRXF2964xlq3rK9zEcmsqmGcFobUsSpvU1TIm7aWGaSVN8ZUHB7Gub1C+YSZDYxUdp4kMTCOc7l9fShYWbXNEHi6alyyItYF1bsd6kr6iuUubqMkg8Gu9u7yG6h7MpFcRq1jEzlovlPpXrYOSfuyVmePjYv4oO6MSZQ5O0g1mzwuM1ZlSeFjkHHrVdpz0Ne/BNbHzdRp7lJoiO1V2j5q80g7ioWZTXVFs4pRRTKYpu2rJxUbCtUzJogIqMrU5FMIq0yGiEdfzoqTFFXczI5o242jtUH2V26itc7DxjFNKxg4LCpVRo6XSTMlrcqcEYpVtRIcZrT2QMeZasRW0K4fdxTdWwlQuzNj0xCepJqwukhv4QK2ovsxHbirUclv2HNcssRLodkMNAwl0hwOBxSjTQPvdK6HKuMAVE1s79Tge1ZfWJdTX6tHojENnHGMhBj6VUmWUnEURNdMLMDAJB9qsC1iVcMBTWIS8w+rOWmxw7adcStufP0FKmkuW5DfjXaPFEnIQGoiob7kRP4Vf1qTM/qUepzP8AZrhfljJNV30+9dsbcL9a7FLSRvvKQKmW1iUYK1P1tor6kpHFxaKzt84OavR6NCnfP0rpWhtxxSCKEjCjFS8VJlLBwic8dKgbgqani0lVGEQBfWt5LeHrgsamHlhcFKzliZdDWOFjuzmZNMRASBuNZ8mhXd04WKPaK7UiEDAjyfahfOHCoEHrTji5x2FLBwlozmIPCEaKrXb8dwDWxFNpmlRiO3gTI74pb6WOFSTMXf0zxXPSwXN7MBuwPQVa5qyvUloQ1ChpSjqbcmoR6ixjMZY9MCok8LxsTNPhAexNSabZvYrubr6tReX8j5/egD61neSfLSdkaWi489ZXZZt7LT7A5UqT6CpZZrqcbYsxx1jwThfnY5+op8+qBFy3C+uaTpycr7spVYqNtkXVso1ffczFj9eKSaNWyLeHJ9TWR/bBd8Rxb/c1aTUrjGWVUH1q3TmtWZqrTeiGHQLi5k3TS4B7CryaHZW6cxhyKpPqzA8SH8Kj/tC5m+VFc57mqarPd2RClQjsrsmnHk527EUelZsuolCQsZY+prTj08zENLkHvuNWfsGnwjL/ADUKcI6PUUqc5arRHOtd3siny4iM+gql/Zmp3b8qwz611E99ZQDbCgzWdLqNy5wjgL7GuiFSX2Y2OWpSh9uV/Qzf+EZnGDNKq0j6JCgwJQT65qeaeZ/vTH8DWdOrMMhj+ddEXUe8jmmqMfhiI+mwR533CfnVaSO1QYSTeahYHPPNNxXSk+rOCU4vaIhC9qZ+FSYFJtFWZEZzS4qTaDRtFO4iMikxUhFNxzRcVhuKMU/FGKAsREc0hFSYpMUXAjxS4p4Wl20XCxHijaBUm32oxRcBm33pCKlIpuKLgR4o2+1SbaMCi4DNtLipMUbaVx2I8UbakAo20XHYjC04LT9tOC0XCxFijFS7aNvtRcLEeKMVLtpdtK47EJFG2pdtKF9qLjsQhaeEqTbTttFy0iLbRj2qbbRtpXKsRhaULUm2nbaLj5SLbRtqXbS7aVx2IdtKFqXbS7cUXHykO2jbU22k20XDlIdtAXmpwtPAAouUokIjPoaPKbstWBT8VPMXyIq+WR2o2VcxTSoPajmDkKm2nBamKc9KXbRcXKRBRS7alC0u2puFiIClA9qk2+1KFouFhu2jbUoWjbU3HYjxRtzUm2gL7UXHYaFFLtp+2lApXCwzbRipMUYFK4DMU8AUu2nBKVwGUAVbtrQzHLHCirwtLZRjy8n3NZyqJMtQbVzGxVmKzd+T8o960Rb26tuEQBqURg/dPNZyq9g5LFEWKgf639KUWDH7rg1fltZYohKyjafepLZ40bLoT+NZOo7XTLSV7NWM2XTrmJNxTK+o5quke6QKWCjPU9q6qTVPItGCBQcdcc1y7MXdmbqTmnSnOafMi6kIxtyu5rXGn6XBY+Yt80k5HCqOKyM4o570AVcYuO7uKUlLZWAsTQAWNPCZNWI4uenFU5JCSbHQ2wYjPet6zt1jAO0Cs+EqgFWDfBF4NcNVyloj0KSjDVnRQypCu5mxTZ9c2KVirN03S9X1tg0MZjt+80nC/h6/hWpqXg67tIVurSRryFR+8AXDKfXHcV5knQjU5KktT1orESp89OLt3M1Zp7uXfIxIrSj/AHajoRWZCyoODxVhZsnrxW049EZQlpc0klI6GtS1uysXJrAEgwBmrkTkKBXHUpprU66dVpk+oXBbPNYskpOa0LnlaypRgmtqMUlYxryd7lq01SS3baxJQ1NeTLNCXjbNYzUgmZOM8Vt7FN8yOdV5JcsthwugSY5lyPWq1xZxyKXhI+lJNhm3CoPOZTwa6oxa1icspp6SKUsbISDUBFab7ZBz1qpJCQeldUZdzjnDsVCDTCKsFKYRWyZg0QEUwirBWmFaq5LIwoop4HP50U7k2L32S0kI2zHOORTv7Osg4Z5Tj0NU2ilX7jAfjSATbvmbPvWdn/MdakusTR+z2Ea/u8KfUjNU5pFjPyqT9OlSKseMu+afiLHynPtUrTfUu91poUftT54UClF3JnOale2VmyvGe1M+xv121peJk1MtQ6gQAG61ZF9LjGePUVnLZuedpq5DbyLxsLD6VjOMDeEp7MtR3POWJq2Jg3IYH61WEW4YKY+tTJaKxzuwK5pcp1xch4nj3YYZNXYXVhlAKgWyyPlOfrUqW08ZyVOPasJOL2OiPMt0WsFhjH5CnfY3bpnmpIGKnnrV+OXA+YVySm1sdcYp7lBdMP8AGOfXFP8A7OjFaPnqKikmIHyqB9az9pNmns4IqizVeQuKabVP4iAPSoprickkMAKos80pI3fnW0YyerZjKcVsi3NNb24wpXNZc00t58sJKD1xira2dpnfO+T9anFxYwDAXIHrW0Wo7K7MJJy3dkZcejQsd0zM59+lSyWgtk3IEUCp5dYt+kYWqpu/NJ3qCp9K1TqPWRk/ZLSJzmoXtx5zBpsJ2qqkby4ZZGZu3FdUbKxkG7yBu96y7yeSzlCKiBPYV3U6qfuxWp59Sk1703oUBbam7bUYhfXFObSwp3Xdwc+ma0LdrqdSRcLt/lVa50+ZyS8ob3BqlN3s3YlwXLdJsSKKxhHygk+tEjwOThPzNZz2EyH5Zcj60o86FfnKY9605E9b3MfaNaONi4htt20ct7Cr9uUX7qkn6VkRThucBfenO+DlZ2z7VMoX0KjUtqjWuZpEGeB+PNY897jIMppnnDdhjk++aZI/HMSVUKajuRUquWzKklyGz8pNVWk9ARVpju/5ZqBUJUegFdcUkcE231IvNkzwTUgllxjNB2jtmmkjtxV6GV2uox1BOTTGCY4BBqb5CO+aiYc8U0QyPApMU8im4qrkjaKd2oAouIZjijFPxRii4rDMUYqTFJii4WI9tGKkxS7aLhYi20u32p4Wl20XCxEVo21Lto20XHYix7Um2pitJtouFiPbSbeal20oWi4WI9tLtqXbRt9qVx2IdtOC1Lso20rjsR7aMVJtpwUelFw5SHbTttShaNtFyrEW2jbU4TNKEpXK5SHbQE5qfYKcEpcxSiQhOOlLsqXZS7KVyuUh20YGamK0m2ncdiMLml2VKFp22lcdiHbzRtqbbRtouOxFto21NtFGylcfKQ7aUJ7VYWL16VIFA7UuYtQKew0ojJq2UB7Uwx+lHMVykG3BpRUmMdabjmncVgpcUoBpwFK4DNuaNtSBfWpNgNK5XLcrhfanbakKYpdlK5PKRbaXZUoSnbaVx8pBtpdtT7KTZSuPlIQKXFWFgZue1SfZwOvWpckLlZUxS7Ce1XFg57VMIV6ECk5ofIzN2kdqUCtE2wYfKMUxbY+YAR35pe0QcjHQWQZA8uee1T/Y4O6frV/y9oHIphUZ5rl9o2zq9korYsQadH5KiMkGq91aTW5+dSPTIq/a6j9nj2+UMjo1Q3l4bkl5JcnGOe1c6lU5tdjdxpuGm5Lpuo6daWUsV1pKXUz/AHZGP3azS+ZCVQKCeAO1RebCOsgNMa6jX7oJraNOzbXUwlNyiovZFiWQmMl2yBVT7VGB8qkmopZXl68D0FR7a2jBLcwb10HSyvKeeg7VHin7acFrS6WxNhgSpFXFOVKeFqWy1EQLxTt5AwKmtrS5vZxb2kLzyt0VBn/9Vdxo3gONNs+tSCRuot4z8o/3m7/QVxYjF0sOr1H8up34XBVsS7Ul8+hx+m6bqWrz+VYQM4H3pDwi/U13+keC9Psds9+RfXA5wwxGp9h3/Gulhght4Vgt4liiXhUQYAqSvlsVmlWt7sPdX4/efXYTKKVC0qnvS/D7gAAAAAAHAA7VbtG5dR9aqVNanFwvvxXkHtnEeM7AWutRzwxBEuI8naMDcDz/AErnDPBb7RcTxw7zhd7Bcn2zXV/FoXsXw/uNQsZTFNZyo5dQMhCdrfTqD+FfM4u5Z7oyXMzyyN1Z2LH9a9P+1vZU40+W7R8Tmn+z13Zb6nvIX5sHjmr6jaBXM+DLkX/hy3ZnLyQsYmJOTx0/QiusKDivWjWVWEZrqOi+eKn3GuhZKy50Iat4IDHWddQ9aqnPU0qwurmM4wKrNV+SOqzpiu+LPOnEpkkcVEwzVl1qIoa6EzmaIMFTS5B61N5ZxSeVVXI5WV3izyKgaP2q6VIppUEVakS4lApTSh9KuGOo2StFIycSqEP6Gip9vPPoaKvmM+UgaxuQwxkcUotLocAk10sdzC2N0Y6VaQ2x5MYx9K5ZYiS3R6EcLF7SOTTTrlyODVyLR5sgls/Wurils+gUflVoeQwyqfpXNPGT7HVDBQ3uczFpL5zsNXksHVcbRj3rdSPI+VMU/wCzyN/CK5ZYmT3OyGFitjnvsBHKIM+lKsUqnBjzXRpZk/eqdbCPqwrN4q25qsL2MOHTlmX5xjNWxpESjgE1qC3jj6VJx2NYSryb0OiNCK3RjHTXQ7kfp2qVCifLIK02Td0FZ9zaSMcqT+FCqc2kmJ0+TWKJlSAjKqM1XldY8mqpS7j4zxVO4a8YECIk+taRp3e5lOrZbFiS7j5w2DWfPfso5bI9qiS1uJWy6Fj3GelWE0VpHBMe3/gVdijThuzicqk9kVI7u3kb94zZq6GRl+SEketX49PEKYZU474zTZBFGOW/IYqHUi37poqcor3jIubZnGScfSqE9qEX55uPc1rzXFsgOVLVj3eoRKD5cKfiK6qXOzkq8i1bKRlEXyhgfTjNMfU5VG0R8+oqnNdTSk7RH+AxVGZ5up4+lelGknueRUxDj8JqrqlzuwWyPQioruWOUCRnZD+dY5aQ/wARpEWR2xu59619ik7o5/rMpLlepu21+Y02xqG+vFSi8ZjkgLWS1qyqCJFOfekMC95z9BWbpwbubKrNKxfnm3/xDHtVXYjnO8VAyIvG4/iaiY/3TitIwtsYyqXepeyijk5FMa5C8KBiqOWPU0mDVKHch1X0LD3APTH5VC07n+Km7TTSKtJGTk2Bcnqab170YoxVEMTFNNPwaNtO4hmKbzUuKTbTuTYjxRipNtBWi4WIttAWpNtGKLisM2+1GPapNuaNtFwsREUYqXZRtouOxHto21Lto2ilcLEWKMVNto2UXHYhx7UuKk2UoSi47EW2jbU+yjYaVw5SDaKNtT7KNlFx8pFtpdtS7OaXZSuPlItvtRt9qmCc0uz2pXHykO2lCVMEpQlFyuUhCU7bUuw0oX2pXGokW2l21JtNOC+3NK5SRFtpwU1Nto2Url8pDso28VPso2UXHYg2UuzFT7KNlHMHKQbaXbUwT2p2z2ouPlK+0U7ZU22k20rjsRbfanonOTT8cU5AdtFzRLUMAdqTA7U/GaAKVy7CLA8n3QTTXgkT7y4q3FdSRIUQ4BphnY9eam8rlNRsUWXJ6Umw+lWv4s7RS9eoq7mXKVgmKcF54FT7M9qsW9lPcMRFGSB1J4AqXNLVlKF3ZFIJTguKuy2oiO0yBmHYVF5LHpUqaZTi0QFcik24q4lrK3RKHgZWwRzS5lsDg7XKgWnYFSmMjtSYp3ItYix7VLFDvbpwKMVdt1GwVEpWQ0rsQRYHSkePA4q0FxSsN64I/Kufm1NuTQoR53YIqwEHc0108sbs00Seoq99UZ2toyyjKo5XNV55Tv8AkOM9RSGQ9hURBJyaIx6sHJ2sizDdlQFlGR6irIlhfkSCs3bRtocExqcloaLPEB98fnVOeRXG1enrUeKXbQopA5NkWygIfSpwKNtXzEcpCEp2ypse1LtqblcpAF9qcFGalC1t6R4X1HVsSBfs9sf+W0g6/Qd6yqVoUo803ZG1KhOrLkpq7MNUJYIqlmY4AAyTXW6R4JuroLPqjm1iPPlj/WN/8TXX6ToGm6OgNvF5k+OZ5OWP09PwrWr5vFZvKXu0NF3PqcHkkY+9iNX26fMq2GnWWmQeRY2ywp3I6t7k9TVvvRRXgSk5O8ndn0kYxguWKsg/GloFLipKG05G2yKfQ0lJQAnibTV1nwtqmlMuRdW0kY/3tpx+uK+LlYrIu7gg4I9D3r7mRgY1c+lfGHjLTv7I8da3pwXasN3JsH+yTuX9CK5q62Z8rn9PSFT1X9fidR4J8TQaNO9ndRk29y64kB/1bdMkdx0r27S54zIkjIrKGG4HuO9fLsLkpwecV9A+Fr/7dpVrcg8SxAkeh6H9a3o1puHs29EYcPVoz5qUt1qvTqdnqtjFZTIYRiKQEgeh9KyZ4lZeldHcqb7w4ko5kh5/Lg/pXOPkrya9vDTco3b1PUxVNQm0lozKngGTVF4ea1pUqk6dQRXrwkePUgjNkhxSLbr1c4FXGSmFa6FM5XBFRo1XgCoWFXGHbrULJWkWZSRVYVEVqyyGmlTWqZi4lYrTCvtVkpik2+1WpEOJU2c/gaKtFB+lFVzGfKaX2AKwGecDpU4sW2cA5rQFsinPJOBUykIeteXKs3se5GjFblC3sWU7mU1eMgiGNual86MDk1A00W7gg1i5OT1N1GMFoSJcyFvuYq2s/HJArOM/oQKjM2M72FS6dy1V5TaW4UdxSm6UD1rnzeovQ006ko70vq7YnikuptyXRP3VpiSuT852iufk1cqPlFUZdXuWOFbGa2jhZMxljIx1O4FzAi8uOPeqVzrNtETiTPsK5YSu4zLJn6nFTxLbOct/OhYWMdZC+uSkrRVjVGtrK2BDn3NTC73feQKKyiYUX92FGPeq7XTA8uF/Gr9jF/CiPbyXxM3TNFjg/lUbXtvAMvKo+pxXNT3k6AmKRSayJ7x5VP2lQx9q3hhObdnNUxyjstTsZ9ZUofJ+fHpWDNqGpTybY7UAHuxxXPG5kU/uiVFSDUbzbjzTXbDCKGyPPqY91PibXodC1rdGEM7IhPvWdLbSFiJJowPXNZTXdwx+adz+NRNJI3VzW0aMl1OeeJg9kaTWligybhs98CoPLtWOFJx/tHrVMM31o3HNbKLXU53UT6F57e2VRgpj1AzUR2r90q2PUVAHOODT1mkHQg/UU7NB7RPoOd3blVQD0WoGD91xUxEsjZCjPsKkEMzHBAX6mi6QneRS2n0pfKY9BWkttCvMsy59BzQ6s3yRMgX1zRz9hez7maIjnng00x89K1RZFRvlYEe1RFIXOEO0+jUKohuk1uZpTApm2tb+zZXUsNv0zUf9nS55XFNVI9yXSl2MzbRt9q0GtDH981H5BPQVSmiXBrcp7aNvtVswEDnimeXinzE8pBtoKVPspNlHMLlINtGyp9lIVp3E0QbO9Gyp9lG2i4rEGw+lKF9qn2UuylzD5SHZRtqfZRsouPlINlLsqfZS7PalzD5SDZSbasbKNlFx8pX2UoSp9lL5dHMHKQ7KXZU4jpdgpcxXKV/LpQntU+yl280rj5Svs9qUJVjbRsFHMPlIAlKEFTbKcE9qXMNRIduO1Ls4qfaaNppXK5SvspfLqcLTtntRzDUSt5dLsNWNlLszS5h8pAENOEftUwSl20uYvlIfL46UeXU2KXbRzBykOyjZU22jbSuOxDso21MFpdmaOYLFfb7Uban2UbBTuFiDbSqMcVLspClHMNaDcelG32qQLnjvVhLO5cfJbyMPULScktzRRvsUttARicAVrLo1+y7jbMo/2uKZ9lmtmBO3I9Dmp9rF7Mp0pLdC2ehX92oYII0P8UhxV2Xw0YFDSXae/aoftl65H7xuOgqOU3MpBldifeuZyqt/EkjpSopfC2y/DptvGAFZG9+tWHsJjFthYKD2BrKSaSIYYke5pw1RkJG9j71k4VG7p3NVUppWasWxo+DmRgfWlMVvD/qrcu3qRVVNWIOcFvqadJqruMbAB7U+Wq3qLno9AnM33iFjHpWa5LOec+9PkleU9SM9qckPy5NbxXKtTnnLnehCEYnnpT/KHpmptgp6hF56mm5EKJTeEDlaRGKH2q6xDNnbiq7phqalfRkONtUOEqnvj60pkAHL1Ds9qNlKyC7Edy59qaFNSBKXbVXsTa5Hilwak20u2lcdiPbRtqXbxQFzSuOwwLxS7eelSBKXbSuOxHijFS7Kkht5Z5ligiaSRuAqjJNJyS1Go30RXxxV3T9MvdTm8qzgMhH3m6Kv1NdVpPgottm1d9o6+RGef+BH+grsoIILWBYLaJIol6IgwK8XFZtCn7tLV/h/wT3sJk1Sp71b3V26/wDAOe0jwhY2JWa9xd3A5wR8in2Hf8a6Wl/Clwa+ZrV6laXNUdz6uhh6dCPJTVkVNQ1Gy0qwe+1C4WC3TALN3JOAB6kntVvoea8H+NWq3TeJrLRpGZLS3t1uFUHAeRiRu/ADA/Gq0Pxr8SxafHbPZWE8qKFNxIjZbHcgHGa86WKjGbjLoeFUz+hRxFSjVTSjs7Xu+v8AwD6BpccZyMDkk9K+e7Xxr8VvEchGhpJKPW0sl2j/AIEwI/WrOp6z8V9F0yePxbZ3c+kXcbW87xJGXjDDGQyfdPPfg9KaxMWrpMqOfUpx54U5uPe2n3nS678WLh9VbSPBelf2rcBignKs4cjrsReSPc8VFa6n8bbqXa2k21qp53zwxoqj3+bNZUXjTwz8JlTw9YaW2qa75atqNwXEYViAfLBwTwCOBwO/NdP4y8cwXnwZXXNJEkLax/oyK5+ePORIOPQKRn3rnjVU2056rojtxeTZnhsCs0zGcoQkrpRaS2ul1evdk3w61fxN4hvdVvdX1eG7sLR/ssP2eJUjlk6swOASAMY+teh1y3w901dK+HejQKgR5IBcSY7s/wA38iB+FdTXdSTUFc2y+E4YaHtG22ru7vvrb5bF+D5rYD04r5n+Omm/YviJHfKuEv7RHz/tKSh/QLX0rZsNjL6GvHf2gNMM3h7R9YVcta3LQMf9l1yP1X9aVVXic2cU+fCSfazPA7dyU+hr1v4Z6nv0+exZvmtpNy/7rf8A1wfzryCA4Yj1Fdj4Dvfsni2GNmwl0hhP16j9R+tY0XZo+Hyyv9Xx8H0lp9//AAT6b0CdZFmtWOVYbgP0NYt5AYLqSA/wMR+Hal0e58i9t5Cflztb6HitPxFb7LmO4UcSLtP1H/1q9nCStPl7n6DjYXhzdjnJABVR+tXZEOM1UdOa92DPn5plcjniomAqwwNRFc1umczRAy1Ey1aYVGVrRMxaKrJ6VEy810+j+G7nVVE7OILXJG88lseg/rXTx+DNEWIq6Syt13NIQf0rmq4+lSfK3d+R1UcurVo80VZeZ5eUNN2Gu51HwO0fmz2N4PLGWEUikkD0yOtUdE8MX1zfRyXlsYbeNgzCVSN49BWix1Fwc1LYyll9dTVNx1Zn6J4dvdUmSQQlLXnMrDj8PWivWFCRqEVVVQMAAYAFFeDWzGtUleOiPo6GV0acbS1Z5dMxU9ccVTe9hTIaQZrVktYW4cZ4HU1Vl062fjYK9qEodTw5xn0MWfUkJ+VifpVdNSbeABmtWTRYzysdV20pkU7IcV2RnStY4ZQrXuxySySDJwPrUhQY5YNWZKL2IkBDgVV+2XIOGBFWqV9mQ6yj8SNOXYD94ZqhMz9F5+lReeX++nNPWRB95TWqhymLqKRCY5SMk4FV3lKH196vN5TcsxFQtbI/3Mn61tFrqYST6FFrh34yfzpxkmKgLNj2zUrWLg0w2zjoK0vHoYWmtys73APMjD6GmCacf8tW/OrZt5B95TUqWKMfmJH4VXPFbkck29DPMkjH5nJpCGPU5rcj063A+aN296Q2tiCRllPpU+2j0RfsJ9WYPlUeXXQro8cyhonY+1H9gy59RR9Yh3D6rU6I5wx+1Hl+1dMdEGOc5qF9EYcqx/KmsRB9RPC1F0MDyz6UmythtKnXtmoTp1yOsdUqsX1MnRkuhm7akWLjJIFTvbyRnDLimbcdqvmI5bbiqsQ+8WP0qVZLdesWfrUaxlj1A+tIUwcZzU7lJtD2cOcpCMUiyFf+Wa0gLKMDpSYNAr9R/nluG6e1R/IGyKXbRtphdvcV5ZOiuQKZluu45+tO20u2loDbY3zD3GTSGUnqP0pxSk8v2p6BqNyp7GkIjP8ADTtlLsouGpCUHpSbKn2Uuw5p8wuUr7KTZVny6Ty/ajmFykHl0eVVgJS+XRzBylYR07ZU+ylCUuYrlINlHl57VZ2UbKXMHKVvLpdntVjYPSl2H0o5ilErbBS+X7VZ2e1GylzD5Sv5fHSl2VY2GnCOjmDlK2yk2Vb8sUnl0uYrkKxSjZ7VZ2YpQlHMHKVtlAjqz5dHl0uYfIV9ntTgh9KsCPFPC+1LmKUSv5ZNJ5eKtY9qTYD70cw+Qr7Pal2e1WNh9KNh70uYfKVtlKE4qfZ7UbTRzAokW2k21NsNOWPJ54pcxXKV9lG2r6xJ3GaX7OD04pc5fIyhsOKcIWIyF4q8LRieMVft9NJGZWz6DNRKqolRouT0MQRMTgKT9BTvIbuprqbfRY5T+8kKD0Wn/wDCPlpMRudtYPFwTtc6VgqjV0jl4rCeY4RCferw0R0TdM+32Aya6uDSJYQBGB9atrpUjkGUg1yzx2uj0OyGX6arU4VNKeSQLGrYPrW5Z+FYXUGckn0rq4dLjQZIGati2RR1rkq5hJ6RdjtpZbCOslc5+Hw5psBDeWMitKNbW3TCxjI9quMsCj5mH41WaW0Y4VlY+3NcbqzqfE2ztVKFP4UkZV6JLvKqpVfYVnrpMEXzPDvPqTmunUK/3QMU7bGvULWsa7iuVGUqCk+aRy7WkrjEFoqj1xiqkuh303JKKPrzXVTSc/IBn1qhK143CofzrohXn00Oaph4dbs5xtCMY/ebpDVV9FJPDKo9OtdDJBfv/CPzqlNYXrH5mCj2NdkK8usjgnh49IsxX0wRDJI+lVGhwcVunTZOjzcfWonsEQcKT+NdMay6u5yyodlYxhHg1MAastbvnCpT1tpQPmiNaOaMlBoqBcmpBDnk1aFu3ZKikDR8HGfSp5r7F8ttWR+UM1BKF38c1ISx61HtyapaGUnfREZFJj2qbZkUm30qrk2IwtLsqQD0pwB9KLhYh20bam20m3FK47DNtKFp+KUKaVx2G7aTbg1KBgZr0PQNEtLSxt7qW3DXjqHLPztz0A9OMVyYrFxw0OaWtztwmDlip8sdLbnNaV4Tvb4LNdE2kB5+YfOw9h2/Gu3sNLsdLi8uzhCE/ec8s31NXKK+VxONq4jSTsux9hhcBRw2sVd93/WgUtJS1wnoB0rGvvE2k6f4l07w/dTql5fqzICcBQBxn3Y5AHfFbIOOa+cPidZ6s/xE1K4uLO5MUpT7NIsTMrRhQBtIHrn8awr1HTjdI8fNsdUwVFVKcbu6Xy6/5fM9w8UeHPCmt2Ky+KI4UjtwdlxLL5LRjuA2Rx7V5BqWv/DbwxIy+FfDia7eJ0ub5maFD6gN978h9a53T/APjfxOUdbW7Fuoz9o1CRkjQevzc/kKtvceFfAjmOyWPxZr6cfaJF/0K2b/AGF6yMPU8Vxzqc/vctvNnzGKxs8RasqUaa/mkrv5aa/c/VF5bn4qeL7L7XcXn9jaKBgSSSixtVHt0LfrVHT9I1Gw1+0l0LxtYavdCZfMt7O9cO67huADgB+M8D8q6DTfBHjHx7OmreMb+aztW+ZFmXMhH+xFwEH1x9K7eP4QeCIbZVjhvRcqQy3X2kh1YcggAbevtQqU5+9b72Ojl+JxLVXlfrKTTfole3zuZvin4L6N4k8Z3HiBNZmtI7txJcQLEGJbABKMTxnHcHFUfi3pdno/gDQdM0u28mxtLgxIgOcZjPJPck5OfevXVTAGTk45J7+9ee/GMoPh8CwG4XkWz64bP6V0ToU4RlKK1Z9zxDmOLxuVyw9eo3GEdF2t+fbU6fwffR6h4G0S5h+61pGp9io2kfmDW7Xn3wemeX4dKj5xFdzIvsOD/MmvQK6KbvBM5sBVdXC06j6pfkWrRv3pHqK5b4q6cNT+F+tQhN0kEQuU+qMGP6A10tu224TnrxVm9tEvrG4tJB+7niaJvowI/rVNXVjor0/aU5U+6aPhpTh1NaFpcPaXsF3GcPDIsg/A5qjcQSWlzLayjbJA7RMPQqcH+VWEOVBz1FcEdD8gr3jJSW6PpPTLlLi1jmjOUkUOp9iM13M+NQ8PrOOXVQ34jg/1rx34e6h9r8NwRM2XtmMJ+g5H6GvXPD8oe3ltX6D5gPY8GvThKzUkfrOHqrFYeNT+ZHPyJmqskZ71sXEHk3Dwn+A4rOnA3HFe/TnfY8WpC25QZOOlQslWXzniptO0241K5MUQCqv33PRRXTzqK5pPQ5eRykoxWpmFa6HwzosF6JLy8TzI0bYiHoT3Jrdg8L6XHGBMrzvjlmbH6CrlrYQaXZtDbltjOX+Y5rzq+OjODjTbuelQy+UZqdVJos/urSBUjjVEUYVVGAKpSXmW75qKWR26nNJDEzN8ynB74rgjBJXkem5t6RNG3nDrgnLVY69KgzDbRgnvRHdwSkhW5Fc7V9UjoTto2CvGJSpfn3orH1C6RXYiXDjsPpRXVHDOaucssSoOxyCx6jLEZVsp3jA5YIcDiqn2pieletE15z43ijt9XgkijEZljy5HG4g9frXqYTFKvU5HGx4+Mwjw9L2ilexRW6buaf8Aa17/ADVhiVvellE8b7JEaNsZwwwa9T2KPHWIZt/aIW6qKiZLSTnaufpWODKT1p4Vz1al7FLqV7dy3RpGxtWHAX8KYdNhPBjBA9KqIpB+/wDrU67x0kP50NSWzGnF7xBtIiP/ACzAFVJdKYH92D+FaSTsn8WamF2GG0gH60lUqRG6dORl2+mHP7x6s/Y7WLksrN9KneRc4k4B9KQTWUfOCx9qHObEoQWhTYRlvkhLH2FSLaXMmCtpx7iri6rBFjyIFJ9WGKsx6pLL1jCj2qZTqLZFxhTbs5EcVpiPEkAH0qCTSLV23FOfataOfzO2asLCH+9gVy+2lF3Oz2MZq25gyQNbxgRKoI9agGpGP5ZowTXTPawMMHmqc2mWbnLLnFVGvB/EiZ0Jr4GZSXtrKuQMH3oaWP8AhAq8beyh+7GMiq8xhwSIxWilFvRMxlGSWrRSeVTxgVUlJP3f5VaZ0B/1YxTDPj7qAV0x02OSbvuzLe1Vm3OrMagmtwB+7gx71sGVifu/pRliPufnW6qNHM6UXsc01tIWxjn0qRdOlOCK3CCDnYM+wpD5h6LWvt30MVho9TENjKvUcioTAwOCprfMUx7DNNNpI33sCmq3cl4fsYBi56Unl1vGwHU81A9nt6LVKsmQ8PJGT5Z9KPLrQMGOoIqMxVfOZunYp+XRsq15dJsp8wcpW2UmyrXl0eXRzC5Ct5dLsqz5Zo2Yo5g5Ct5eaXy/arWyl2HtS5x8hU8v2o8urWxj0FPET4+6KXOPkKXl0eWfSrhibPIo8puuKOYOQq+UfSl8o1bER9DR5LGjnHyFUR807yx61bFrIecU0wMDyDS50VyNdCv5Y70eWOwqcRtnG0n8KlFtORkRGk5JAot9CmEPpTtgq2tpKf4CKlFm/dDSdRFKm+xn7B6UeX6VeNuM4Gc+gpwtG25OFHuaXtEV7Nmf5Z9KBHzV1oUX+MH2pmwk8Cmp3JcLFYpSbKt+S5GQh/KlW2cn7po50HIyoEpwj46Vc+zP3U00x7evWlzlcjRWMeKQJ7VYKmhUyeaOYOUiCcUFfQfjVxI4zjORUptUfG2QfjUOdtzRQvsZojJPFTpYSuudpx9K17awxhsg1fWNl4yBWM8RbRHRDDX1kc4lhubByMe1TLaQhtoRpD9a3ZIGdDtqmqMmV2gGoVZyNPYKJnmOJDgxfkaUpEeiEfjWtBYFm3lOvrVowW8R+ZQT9Kh10nYqOHk1cxoreLqWP8qvxRwj+LP41ZM1sg5iBPuKga9t8keUv5Vm5Sl0NVGMOpciIwArAD61djfaMl/1rBe8hP8ADt+lV3vIwDtZjWboORssRGJ1n2pUGSwx9aZ/asecLz71xpmLtl2YjsM08vJIuPO8tf1o+px6h9el0R0lzr8cIIDgt6DmsS48RX0pIj+QdqoeTb7sKGc/3jVhIwuMKBW8aFKHS5zTxNappew6Ke7uj/pEzlT74rXtYrWMcOSfc1kmQqMAZqF5bk/dyBTlBz0WgRqqGr1Z0E98YBlGBH0zVFry5ujgXBA9hisXdKT87E/jUgLNx0+lCoRiKWKlL0NhBcRjJugT7mle7kQZa5H4Cs6IJ1dM/U1ZWa2UfNGo/DNS4a9yo1NN7CnUWB5nOPYU06iX/wCWmfqKgney5ZVLE9scVAkXmc4Cg1oqcbXsZurO9kyaS4nk+6vHrT0tbiQZaRQPrTEWCE587n060PeHGEGKdntFE3W82T+UsPG5S351C/mLlgyn61CsshfIODVhU3fPLJRbl3Dm5timVnlJABx/simmyn6iM1qC6hj77h6ClOoqRgRYFHtJ9EJ04fakZYsJT2zTv7MnxnZirzX4H3FP8qry3s0g25wKpSqMThSXUoyW7xnDD9aiKe1WDk+9MKmtk+5ztLoQheaeFp+2lA5p3FyjNtAT2qYLRtpXHykOylCEVMFpwSlzD5SbSrI3uq29tjKs2W/3Rya9P/DA9K5PwlZ/NPfMOn7tP5n+ldZXy+Z1ueryLZH1+UUeSjzveX5B3oo+lFeSeyFFFHSgDkPGHjyx8JX+l2c673uZQ8+BuMUAOC2O5J4H0NZep/GnwnaRMumxXmozdgqeSmfdm5/IVifE3wJ4g13xHHrGj263sbwpE8XmBWjK55GeoOa5/RPgvrt5Msmt3cWmW/UojCWU/THyj8TXDOdfncYo+PxOLzb61OjQp+7fR26eu2vmZes+LvGfj/UF0mzZxHMcJYWeQpHq7dSPUk4r0/wL8MrHw15Wpax5eoasBleMxW5/2Qerf7R/Cur8PeGdG8MWBtNHtFhDf6yVvmklPqzd/p0rZrWnQt71R3Z3YLKOWf1jGS56nnsvT+vQGO45PekI5opRycDmuk+iAZ7V4V8XfE8WqarDoFlIHt9PYvO6nIaYjGB/ujj6k12vxA8YnR9MNtp+q6fYGfdE19c3Kr5Z7rGoyzPgjtxkVzHw48EaJqccHiZ9Zt9bt0kPlpAreWJFPO/cASQexArmrKc/cij5rNvrGM/2LDx0fxSeit2Xfzt6HefD3R5tD8B6faXKFLiXdcSKeqlzkA/hiuqo/HNFdEY8qSR71CjGhSjSjtFJfcOU7XVvQ5rVYnbkVkVqxnfCh9RTNz5E+J2mjSvibrduq7Ulm+0IPaQBv5k1y8DZjx6HFesfH7TfJ8VaXqqLhbu1MTH1ZG/wYflXkVucOV9RXBLSbR+VZrR9niKkPO/36/qej/DO/wDK1q6sCeJ0Eij/AGl6/of0r3PSbkQX8LE/K3yN9DXzF4evTp3iXT7zdhUmAb/dPB/nX0TC+RgNyOhrspu8T6vhqv7TCum94v8AB6/5nXapaBpROpAJGD9RXMzL8xGK6xyb/RFkj5kZQ2PcdRWFc6dexoHa3JB/u84+tevhaitZs9TF03e6RlxWs13cLBAm526V1+l6UNLikxKZXkwTxgZHpWFpTT2+poyxO/VWVRziutkfYvXFLF1JXUFsGCpQs6j3RDLKd4A4AokcOAo5qFTuk5PWrSRqh3Nye3tXC7I9BNsYlvEnzsm5v5U1LxZJhCqc1TvNTWMvEg9s1l/2k8IKqwHWt4UZTV2YyrRg7Il1nUAtwY/vKvGM4rOs9RWFmb+LHrVC6kaWQsT1qlht3Ga9WnQiocrPKqYiTnzIv3d0biYsnGQaKrIMDkc4PNFbqKSsjmcm3dno0Upd8dsVzXi/Rje2yXttDJJcx4QqgzuX1xXUqiKOODiq08AMolSSYMOiq3yn6ivAo1XTqKceh7+IoqrSdOXU43w/4etTaGfU7VvNZvkDEggfTsa6SXStFukRbm1WUr0Zic/nRIt0rZaFiD6c0keQ/JI+tdVWtOpLn5vuZyUaFOlH2fLf1RTHgzSvtazK8phHJhLZB/HrVi+8KaRdQbYofssgGFeL+o71swthPmNSh1YZHNczxNa6fM9DqWFocrXItTxu8iexvprSXh4mKn396h849q9mMFu0jSmCMyMACxUZOPeuO8XaVawxR39tD5UjPtcIvyt7n0Ne1h8wjVkoSjqzwMTlkqUHUjK6XTyOJzK38WKZmUHG41ZMbd+KAig5bJ+letzI8Vplb983ALGpo7G6YZ2EVOkyxdAcU77c44G4VLlL7KLUYfaYyPS5S37xgo+taVvZpGuC2ay2u5m71GJJc53H86iUZy3ZpGpThsjok2w9GH51J9rAH3h9M1zRkkI5YmgO4/iNY/V77s3WLtsjojeeh5pPtpPWsETMDknNPFyTxS9gkP602az3SMOVFVJJEY/dxVYSbu9Lhj0xVqCiQ6rkKRF6Un7qkKOe1IsTe1X8zO/kSgJjil2LSCNv7y1MiHuwqG7FrUiESZzTwq9lqcMg4J/SpVKn0/KocmaKKKvkr1xTWhHbFXiAaaUGe1Tzl8iKBhP92mGNgOENX2Dj7gzUBE/90itFMylCxQaDJ5X86ha3jByQK0jDI33jgVG9svZ81qqnmYSp+RlPBGM4FQmJfStRrY9iKja3IGTWyqGDpeRm+UPSk8ur5ipphPWr5yPZlLyzR5Zq55eKPL9qOcXIVQlKI6s+X7UoSjmBQK4jIqRUJ9qm2jHSlAI7VLkWoiJADyTmp1gQdcUisegqTypX+7is3JmqS6IZsTOAQKVrQMM+aopPssueRip4tPkc/M2B9alyS6lKLf2SBFWFsFgfxzVpJIGHIH5VN/ZUYTJdifamR2KnhWJ/Ss3OMtbmqpzjpYZvtxwQMfSgvB6iri6O7L0NOXQZSeWwKz9pT6s19lV6RM9p41Hy1UknLZwa3/7AA+8+aYdHt1+9IM041qSJlh6z3Rzu5yeBilO9hhs1vG0tIurAiml7RPuRgn3rX2yeyMXQa3ZhiEk/6sn8KnRJBwluR+FaYndjtjiH4U55ZlX5go/Gh1ZPoCpRXUzmiutuDGwHpUHlT54Bq811P2eozdTnvmmnITUe7K/lXBHIJFRNC+fuHP0q4JZSe/4Cms8jDljVKTIcUUTH6ilEdWCntSbcdqvmM+UjVM/w5qVY2B9PrSgsOhoEjL0pNspWRajcqO5+gxVtLlFUFwSfSssyue9J5knrisXC+5vGrbY1ZNSXbtCY+lMivIQ2RGWb3rMLMaZg0KlEbryudB9vdlwERfxqlLdPnmRBWbzjvRtHcUKlFDliJSJ3l3cmQH8KhZkHTmm7KNvtWqSRg5NkZ57UbM9ql204IarmIsQbPSnCPJFS7MHmnhaTkNRHRxqoqVF3HGOKjAqZHIHArJtm6sBgXPNNZUA5PFPZieSKjbDdTUq/Ubt0RXkVG+4tR7GHNXERO9NcqTgDpWil0M3HqVcH3NNKmrJU9hTdtVzEcpW2GnYbGMnFT7BRsp8wuUrbKUJU/l+1OAAH3aOYOUg2AfWhueKm2k0hjpXHykGKQA1PspdmTT5hcpBtpdtWBCT0FOMOB1pcxXIyqV44pu32q55YxSGMGnzC5Cpsz2oCVb8v2pNmKOYfKQCOnbBUoWnheelLmHykAj54FSCJuynmpxlelaGkW7XWqRqwyifO34dP1rKpV5IuT6GlOl7SSgt2dTp1qLLTobf+JVy3uTyat0nWl/Gvj5ScpOT6n3UIKEVGOyCiiioLCiigdaYCUDrxXEXXi/xG/j618IWHhmG1luEkuFvNQuhseCNwruqRgnJzwGI9686h+K2ratr3iTT5biR9K1O1v7fRWtbV42tpoQ4QeYB87SKpYEE4IHSkYOtFaHu91eWdjE8t9eQ2qIjSM00gTaqjLMc9gOp7Vn6L4k0bxEJ5NEunvIINuZ1hdYnzn7jMAH6fw5rxCDwN488SWGg3t/cx3guPCd1YBpI/JNo8ka7ElLMWd2PJbHGDxXpHw28La94R0lrfWXsXdooYwtvPPO2UGMs8pxj0VAAPemKNSUpbaHGt8bbzUJdXt7KzstJeGxvZbSK8WZrppYUYruUoIxnaWK7icYrjrvX9d8ZfDPVS+uXuq6zNHZXAtILxJWVfNG/bBbqBGp3AYZi5HUDFe32Pw98OQam+p6gb7W70rLGsuq3TXIiSTIdUU/KoIODxnHFdPY2Fjplutvpllb2UK9I7eJY1H4KBQT7KcviZ4rp/hTxIsfhe/wDD/hFIJdF1Ka6MeowQ6bHMskIXcI0LsACB97LGvSPAfhWfwnod5Be3MNxfajfTajcmBSsSSSHOxAedoAAyetdXRQaRpKLuH40tIKWkbBWlaHNsPbIrNq9ZH5HX3zQB5b8edO8/wNZ6gFy1leLn/dcFT+u2vm+M4lFfYHxH05tV+HGvWarucWrTJ/vJ84/9Br49B4DD61xV1aVz4DiGly4lS/mX5af5F3HHXFfQHhjUP7S0Cxuy2WeIBv8AeHB/UV8/BsgH1r1T4Y6gZNNurBzzBJvUf7Lf/XBrai9bHJw1X9ni3Sf2l+K1/K57j4duA1tNbnqh3D6H/wCvW5nK8cVx+jTiHUYmJwsnyH8f/r1124IPmrotqfpJCYnSRpY0Xe33iByabMHMe5hj2q0GDd6Cqt1GRVc2upHJpoUbUDJZuB71cLowIVhkVQumaFiAuFPSs6S98skqxrdU3PVGDqKGjI9WTbIZNw5rnpZCzcmtO5uDPnJrLkXBr1qCsrM8mvLmd0Nz0yanSEuhK496pPndkULLKo4JrpcW9jlUktydwEPzN2NFVSJGJ4PQ0VSj3ZDbPVMA9aZJJHCm52wKry3saR5BBOKwrrUHkbGeK+cp0ZTPpqlaMTaj1ASS44A9KklltmcCQZIrlhPj5t1Xba7hYgOCT7muiWHtqjnjiL6M1LwxJAJomIxwQKSyuw3y1JKtvcW3yIAQOoFU7O2PnffBFZJJwaZo21NNGjPO6kbRx7UsYW5heOVQ6MMFSOCKkdI2jxwDTIFK9DwaxuraG9nfU5a48FttZre9DNkkK64GPTNc02m3oeVfskzGIkPtUkLXqrEKpNUor+EyMgXa2eR616FHH1kndXPLrZbQk1yvlPLDGT2ppirvr/Q7G/vi8dz5Er8suAR9RXLXtg9ldvby4LL0K9CPWvWpYqNXRbni18HOlq9u5k+V7U7yjVzy+elKI/UV0c5yezKXlUeVW3Z6Rd3xzBCdmcF24ArZPg98jF6mO/yGueeLpwdpM6aeBq1FeMdDivK9qPLrvh4OtPLGbubfjqAMflWRqXhu4sI2nSQT269TjDL9RUwx1Kb5UzSpl1anHma0OZCGpFQ9zVrywcYFIYsHHQiunnOPksNXywPmNSBrYdajKYpCoqdy07dCwLi2TopNKbqFuPKAqr5dGylyxHzyJS8Dc4INL5kWOCRUGyjbT5ULmZN5sY/if86YZh/CW/E0zbSbKdkJykP+0SrypI/GmG5n/vE04RUeX+VP3Re93IjLKf4jTfMfqTU3lijy6d0TZkGXJ60m1j1NWPL9qBGKOYOVlfZRsNWdntS+X7UcwchT8ul8qrnlH0pfKPpRzhyFLyqPKPpV8QknpUq25/u1LqWKVK5nCHPapBbE9jWmtv8A7IqdIOeUFZusbRw9zJSyZj0wKtR6e+Rj9DW3FbjHKirKW47cVzSxLOyGDRipZSg8nI96m/s9D0Q59uK3EtxUywqB0ArmliWdccKrGEumvjAz+dSRaRsk8w5zW6FRe9DOqisniJvY1WFgtWVI4ioxinlD2FOe4QVWe9APAqEpM0bjHqOaJyMYqlNpzydwv0qVtT29v1qNtUQ+ua3jGotkc85Unuyp/ZAGckmo20hO7H6CrDX5b7uPxpnnyNzvrdSq9WczVF7IpNpeDxJtFNOnRg/NLmrpdT1warSojc+Zj6Vspye7MJQgtkRPBZIvcn61VYW4+6pNPeMf3sio/Lx3raPqc0n5DD/sDbTCv94ZqYqBSba0uZWIdvtTSvtU+zNIUp8wuUrlKTZVjZ7UuynzC5Stso2VZ8ul8qjmDlKvl0vl1a8ul8v2pc4chU8uk2Vb8v2pRF7Uc4+QrCM0vle1XFipTF7Uucr2ZS8v2pfLq15VKIj6UucOQqbDRsq55XtTfKPpRzj5CsFpRxU5j9qTy6OYXKRbm6Um0HqKm8ulEdHMFmRqiVahsopusgWoth9KkVT2OKmTb2ZcUk9USPpLj/Vyo/6VTlt2jba4watguvR6lWJH+Z+TUKcluzRwjL4VYyvLo2Vr/YzIcRrinppUjH5vlFP20VuxfV5PZGN5dHl1uNpaoPvE/hVdrQDhRmhVk9geHlHczBFml8k+taP2cgfdxTDDjrT9oL2Zn+V7Uoj9queVg9KUAjonP0p85PIVBG3YUohc9qt7G70eU57GlzjUCsIR/EcUpSMcgZPvUxiYckU0qQOaOa4ctuhXZM803y/arWz171cttKvbqMyW9uzoO/8Ah60Oooq7YRpOTtFXMoIfSl8s10+m+HpLli94HhjHQYwWrSm8KWD4MM0sR9zuFc0sbSjLlbOuGX1px5kjh/Lx1rpPD9v5drJcEcynAPsP/r1rDw3psUTb/MkIHUtinRxxwxLFGu1FGAK4cVjI1IckD0cHgZ0qnPPoOpaTFGa8k9sWiiimAUhoxR9KAMybQdNuPEtv4hmjke+t7SSyjO8hPKcgsCvc8DmrdlY2Onafbadp9pFa2lquyCGJcLEPRR2qx9KxvFHibT/CHhe+8R6skpsrJQ0ghUM5ywUYBI7kd6CbJalyfUNMt76Gyn1SzivLhtkVu86iSRvQLnJNcr49+JXh74dWME+tedczXDlI7Wz2vJkDOWBI2j3NfH6+KJrj43N428OWL3d3NqrXlnZSgs8hYnah28k89BXpXjX4daSfBbfE34g6je+H/EWq3TzXWlxorGRuQsMKtypICsWYkKM8VVu5j7RtPlO1uf2n/BK6LHJaaZqk2sTxsVslhDeS/IUM2QG7H5c8V5t4O+M3xn8WeLZbLQ5bTVtQltn22UkSR28ABGZcZHI4HJPXvVj4Zaf8VPEXhuVvh1omh6DpkBeI6lcwI1xOx5ZfOdSznnGQAB0rF8M+K/iF8MvGTaDH4e03Ur9sxnT4reKSRu+EkgG4dM4yenSqsiHN6Nn2XYG8Ol2h1HYL3yU+0CP7vmbRvx7bs1YxWboN5dal4d03Ur2yawubq2jmltWzmBmGShyAeDxzWlWZ1IXFFHNH1pDCrVmcTEeoqrU1s2LhPc4pgXpoo54ZIZRlJFKMPUEYr4e1GzfTtVvNPkGHtZ3hIP8AssR/SvuRhkYr5J+Lem/2Z8U9WVVxHdFLpeOu9Rn/AMeBrlrrRM+V4jpXpQqdnb7/APhjkIGzEPbiut8C6h9h8VRRlsJdIYj9eq/qP1rjLdvmZfXmrsE721zFcRnDxOHH1BzWdN2sz4ShVeGxMay6NP8AzPpm1dmjBB5HINd5C/2vT4ph1dQfx715vplws9rDPG2UlQOPoRmu38PXG+2lt26xncPof/r16D0dz9iTUloakcXljduyahkv0icq4xWfI88d5KryttU/L9KoTszyFi1dUKPM7yZyTrcq0Rq3F1DcLjPNZctuzfd5FEbADGQa0bVo8fPya2t7NaGV/abmI1rIO1MFi0jY2k10F2YfK4IBqhFfCJSpHI71rGrKSukZSpQi7NlJ9IA55q5Z6baIpMoDMfXtRLqKuny8NVM3ZzndVfvJKzZP7qLukaP9m2ZztXJwaKrR38fl/exwaKz5avcfNSMk30jNyx6CpopEY5lbj2rNx8w+gqUROeleg4ROGM5dTXf7EY8RhicdSarwg+ZzwKob5EOCasxTk9az5Gl3NVUTexvJcSGIRrwPar1l8hyTk1iwTtj7taEM+O2K4KkNLI76c9bm2ZECgt37U4bcZUcVlfalYepFSreFO1cbpM7FVReYeYCpOAfSs7+yo1d2E7/N2HHFPfUlA5GM+tJFdh345zTipxWhMnCT1Ej0e0Rt6NIHI4JfOK468jkjvpklfzHViC3rXfSxvLblUk8ssMbgM4rAn0NI5Rvu12dyR81dWGr2bc2cOMw7kkoROciiaaZYo0LOxwAK6XT7K20wtNJMkkzLjbtyB9Kr+RaWcZkRWkk6CTOAPwqo0zO+c4rqqSdVWWiOWjTVF80tWdOnmOBKqk+w6VaV2KjehWsSxecYCyZJ7VuIwKhXIJry6seV2PYpS5lccrhiQO1OIHQ8ikARBxhRTWkjUZLCsPQ39SNLW1jmaaO3jWRurBeTUN7plpfRMs0KhuzgYINSNOcfKMimx3DMSrrxWq50+ZMyag1ytaHAXFsbe5kgcgshwSO9Q7BXWXnhsyS+bayg7iSyyHp9DWVcaReW7bWgyPVTkV7lPEwklrqfOVcJUg37uhj7PajZVtrd1OGUr9aTy66Oc5eRlXZ7UeXVry6Ty/ajnDkK/l+1J5ftVny/al8ujnDkK4Sjy81a2DuKPLzS5x8hV8v2pPL5rShsbm4z5EDSY64FI9pLHKIniZXPRccmp9qr2uX7KVr2M/y+OaTYPSutsPD8AAlvW3kj/VDgD61pDRtKIP8AoSc+5rlnjoRdtzshl9Sau7I4ERn0pwi9q7Sfw5YuB5DNCR77gfzrNk8OXiviN43Xs2cVUcZTl1sTPAVYdL+hgCMelOWIVpy6Xf267pYDt9V+YfpVUg47flWyqKWzMHRcdJKxEqqOozU6GIfw1GVPqKaQR3zSeoL3S6HiHagzRf3gKoc03BPal7NFe1fY0hclRkEEUf2iAeeKzcH1pNpNL2Ueo/bz6Gn/AGpjoaP7Xx1yay9lG00/Yw7C+sVe5pnVvr+VB1SM/eLVl7TSbfan7GAvrFQ0m1GI9EY1Xe9jP/LI/nVYJntTxATTUIIl1KkhrTq3SP8AWmGV88KBUpix2o8qtLoz94iMrHsBTfnPerIi9qUQn0pcyQcrZVw3c0bM96t+T6igw46Yo50HIyrs9qbsNXRHnjFTpaKy8kUnUS3KVJvYyvLo2VrtYDGQ1QNbFe1Cqpg6LRQ8v0pfKq9Hb72x3rTi01VXJ+aplWUdyoYeU9jA8n2o8gntXRm1AGNgA+lKkGD8q/pWf1g2+qHO/Zz6GjyD6V1At3I+WLn6VBJYOG3SoR9RUrErqU8I0tDnxCe4pwgrdFlEvLc0q2sUpwkJb6U/rCEsKzD+zcU6O13OBnj6V0sOnvyPs+3Hr3qBhbwyEcAjrWf1m+iNfqnLrIqx6ZBgHJJ96c1jAWClCake7iQ4ByfagXyYztb8qzvUepty0loL9jggXKxqT9M1WeKEAuYR+Aqc3xbhIXb6CrlvAJRuZcH0zU80o6yHyRnpA52SMM3yDFL9ilC7nUqPcV1iWdpCN/kqXHOTUU9yWQps3L7mqWJb0iiXg0leTOTaDHVcUwxD6Vus0zkr9n8wewzSQ6esxO4FD2GK39tZXZzPD3domCYqPKrcuNOe3XcwUj2NUvKBPbFaRrJq6M5UHF2ZR8o+lKISeorTVbdfv8n2qZZrRekY/Kk6r6IFQXVmdDaBjyCwrVt7FQMqoH1ppuoh9xcVG11M4xEuD7VjKU5HRCNOHmaAt4l+8Rmo5poYgQDk+lZ4W5cjeWx9avRJbbcsAT781i423dzojPm0irFRpRKcEYpyrEBlYyT71f8AMtEGML+VQve2w4VRVKTeiRLglrKSKTLK5wEVR9KUWO77xNWDfwgfKv6VXkv2bhVq059FYyaprd3BrKFRkkD61EViXopNNMzMeRTSz9jWiT6syco9EBI6hAPc1C7MeA3HtSneTyamtwonQyx+YgPKjvV7amXxOxVWF5G2oC59AM1taXpkAH2i8Ul1PEbDA/Gtu3kgggBitxCG5wBVW6kDneSc1xyxEqnupWPSp4SNO0m7svs1rJD+9ijZR0BANQrMS2IxtUdAo4FZ0MU07YXO0VpxItuhYk8dcmuOUVHTc7Yyc9diRTk7mbHuadvEjbUPHrWXNfCV9qthR61Ot4EULEufeh02UqiuWroiOEJnlj+lUc5pzyvKQznJxTa5mdK2ClzSUYpDFopKKADNFFFABUVzbWt5bvbXttFcQP8AeilQOrd+QeDzUtR3E9va20lxd3EVvDGMvLK4RVHqSeBTEz4t1yK3sP2tmWCMRLH4jgKog2qoLJwAOnWuk/aN8N+I4fGsWs6pr9vLpd2HFnHcOVFqq4zEiDLMT94kDuMmuJ+IWvWEH7Q9/wCKLC6jv9Pg1SC6WW2YMJQgQsFPQ8qR9a1vjf41vvGem+FfE1/oc2iRTm8ht4JydzxK0ZV8kDOcnpxxxWnVHJpZoxzrmnTaRaWWu/E3XdQiSFUj0nQ7R0SIY4TLsiD3wp/GqFlq+jeDfGGnazYeDtShFrIl3Cur3TpM4B5YBFQc4OOo+teteBfipDYeE9L0vwJ8IbnU9VhtkjuLxIAqyygfMxZFLHJ55YUzxD8M/i38XvFNtrXizS9P8MwRwi2XdJlo4txY4QFmZuT1IouHLfY+mNPvk1PS7TUolZYryFLhA3UB1DAH35qzVbTrGHTNJstMt2ZobOBLdC/UqihQT78VZrM60LmlpKWkMKVDtdW9DmkooA2Oxr56/aB00rq2h6uB/roZLdj7qQw/RjX0HE26FG9RXl/xz04Xnw5F4q5ewuo5c+itlD/6EKyqq8GeTm9L2mDmu2v3anzFH8soNXMcYqkauqcqD6iuKDPyustme0/D3URd+F7eJ2y9sTCfoOR+hr0vRpxDqURz8r/Ifx/+vXhPw0vfJ1e6sSeJoxIo91PP6H9K9jhkxhhwRyK9KDvFH6hkuI+sYKEnulZ/LT8jsdQVI2Wdlzn5TWBMQzkoMCuilxf6QHTlmQOPrWHHbySfw4HrXoYeS5bs3xKfNZdSqAc9aniZhwGp0tuEbGc1PDp0soBBwT/KumU42uzljGV7JAqRtzK+T6Uya3t2U7SBU7adLuAAY9tx4FV7m0mtyA4yD0I6VnGSb0kayUktYme1qck54qBoCDxV7Y1TwQZbBj3Megro9pZXZy8nM9DLW1dugPeiulFlIq5ZVXAPy0Vn9Z7Gv1Xuc41iuflfHAph0+62lozuA96tIzPIMITwK2bS2dgN6FV9xVzrOC1FCiqmiOUFrdEk+WSR2rQtLC6bkxbR71o6hEtpdBomyDyR6Vaszc3CllUAD+9SnXbhzIcKCU+V7mYwEB2yNgihrxVTgFvTArQnsme4/wBLkUp229asW8NjBh4lG4fxE5NYurG12rmypzva9jMhmuTH5hsZtv8AeC04XV/KpNtasy9MkdK3PtkeCQcn2qs9/Au5mADEdutZKo39g0dOy+MzUs9YkISXZsJ5JIOK0IkgsmKzTbvQAYqr9tkKFwxwOnNZrzPI5LsST61ryyno9F5GalGGqu35m0upEvtU4GatiFLlcnJJ61jR/ZlUEZLd6n+2NEP3TVlKn/JobRq/zjrnTSJtu8CM85NUbq1hi/1TMcdc06fUJpcAnkelNQySISw3EVtFTik5Mxk4SbUUMiuHhHytg1Kl9NnIY1TdSDyDTkOCK1cU9TJTktDoLdbi5QGSTYO3fNWxZRheZGJ9c1jW1wy4Ctz71fN0wHzNk1wThJPQ9CnOLWpcSBEG1wCOxp7RrgMoxis9r48A8inpdE/xYrJwluzVTjsaGTt9KrXS7rdmz0p8T71ByDUrKrIVPQ1mvdZbXMjkLkBmOe1Qx2ss7YiiZj7Diulk063CFlQ46n1oinhgXZGhXJ5FeisR7vuo8t4W8rzZjf2JfdfLXH+8Kpy2ckTlJFKsOxrrEuXLfNTbmJLlcNFlh0aojiZp+8XLBwavDc5MW0hBKoSB6CmeS/8AdrrEMcaeXt247VItnBJhnQH2rT61bdGSwV9mcf5NSJayORsjYg98cV1ktrYHl7dN3sKdDbqsZVG47DHSpeL0ukUsDrqyDT4xbW4j6HqfrVmQhju8sZHAbHIpjW7p8ynOKRJMttbgVxv3nzHoRXKlENjHkVZjBC80i7O1OzzxWbdzSKsOo7UUZqDQWs670i0vJfNO6Nz1Kd60KCQBmqjKUXeLInCM1aSuc3ceHpkANvL5uTyG+UiqVxpN7bqXeEso7rzXYBs0uRXVHF1FvqcUsDSltocB5foKTyzXYXOk2tw5kGYmPUr0NRDQ7QpjzJN397I/lXWsZCxxPAzvZHKeWfSjZXQy6DIMmKdX9AwxVNNOnecwhNrDqT0FaqvCSumYyw04uzRleXRsrom0B9mUuFLdwRgVnTWMsD7ZVwaca8JbMUsNOGskZ3l89KXyTn7pq4IueKdsfvzV85n7MpiFhS7PWrXlk0vk56ilzlKBVC08RjGSKupYyMMqhx61J9kkjGWUEVDqLuaKjLexRCqo+6KduH92rexem3FL9nj/AL1TzrqUqb6FAqG7UnkseQpP4VpGKE9qRQyHCvx6Ue07C9l3Msgg9KUZ9a2PJEvLr+NNNjGDkMv0NHtl1H7CW6M5FkJ+QE0945iMMuPrVl4nj+4w/CoCrmhSvqhcttGLbo0T7jsq6LlSQoHPpVJYZW5VGP0Gas2Z8i5zKoHue1ROz1Naba02RdS3mfl2WMejdanjvbeKXykUY7ms+8uBI+UJqmCwbIPNYqlzr3jd1uR2ibsl+POCocLVgXSMBnBB9a5ss5bcTk1YiE7nIO0D1OKmVCKRccRJs2p4LeQGRowTjoDjNV7O9gec20cJjI9utUpbuQDydw44yDmrVmjW0RYsDvOSahwtH3vkaKpefu/MuudjZ+Y+2eBUS2tlLndAhY9cnNQl5ZSxSTr04p8EXkAvJIWb37VnZpb6ml1J7aE3lWcK+WIUx3+WoTLbH5DEFXpwOlMnnj3ZJwaikNqUJVirfWqjF7u5MpJaKxZ32qxbdo2qOARWXJcuZiYl2D2phMzkogLD2FJ5LKf3oI9u9dEYKO5zTm5bF+HMybZZiT3AqaOwskcn5pPYt0rOEnlkMucdKl892H7tce9Q4y6M0Uo9Vc0mkg8oxqAAOOO1UpUuGG2A8HuetJDGc7nPPfFXA8agkcfjWfwPTU1+Na6Gd/Zs0rfvZtv05NRXWjSRRtJDI0oH8OOa1FnBFPilcNnnFX7WotTP2FNqxyux8ZK4+tMKe9dozxOnzKrD0IrKvrSKXElvFh+4Axn8K3hibuzVjlqYTlV07nP7cds0/c4HynbV42cy8tEwH0qWOySQjcdvsorZ1Y7nOqMr2Rll5T1Ymm7pPU1vLpCk5Z/l+nNMk0rDfu3yPcVKrw2KeGq7mHgnuacEb0rbTTgvUAmrSWiKPu0niIrYqOFk9znltpG6KacLSX+4fyrphCgHQUuwVk8S+xusGurOZFrKW2hDu9Kvroz+VuLjf6VsAKDkgUjOo9qiWIm9i44WC+IpWGmxxHzJ1V26BSMgVbdLSNxKIUR16ECo2k54ao2jD/fNZtuTvJm0VGEbRRXuLrc/BzUSHzHG7pVh7aED5Qc/WoSnlcoRmtouNrIxlzXuzQ3qkO2IhDWXcyTtnfJkfWh3nYYBqAwSufmY1UIKOrJnUurJDoJYonzIoarkV1G77YRtJ9KzzakDk5qzZweWWc9TwKKvKouQUeZyUbF6kozSV5Z6wvFFFHagA7UUfjR2oAKTNFLQAdaxPFvhqx8Y+E77w1qU0sNpehVkeHG8AMG4yCO1bVVNVTU30S/XRpI4tSNu4tXlAKrLtOwkHjGcUxM5Pw38JvAHhMpLpXh+CW6TpdXv+kSg+oLcL+AFdJq8Hh4tBf8AiKLSmNsD5M2oCP8AdZxnaX6dB09K+XfDPx+8X+FPG+pWvxSku72GONoJLKKKNHtp1ORtUYGDyDz0INVvirJ8OX+Is+ralB4i1m71i3t9Qhs4JY7eGNJY12JuIZieOQAOtVYx54paHvGs/Gz4ZaCjQv4lguSnHk6dG02Pb5Rt/WuA1D9qPQVDDQfDF9enor3UyQqfwG4149J4ji8IatZGT4O6TZRviZY9aW4uJZos4yDIQB0PIXrWp+0JZ2Nh8WVNjZw2Vu+m2syRQIEReG6AADtTSRDnK10fWXgzWtU8Q+DNO1nWdKOlX90jPJaEMPK+Ygfe55AB59a36gspfP060nBz5kEb5PuoNWKg6VsL0opKKQxe9HeiigDStG3WwHoSKxPG+mHWPAeu6eBlpbOTYPVgNy/qBWvYt8rr6HNWXAI2sMq3BB7ihq+hnUgpwcHs9D4ODZUH1FXLdsx49DU2t6e2l+ItS011wbW6kix7BiB+mKq25w5X1FeZHR2Px6tBpOL3Rv8Ah++/s3xHY3hOEWUK/wDung/zr6CtnynXNfM/UV7/AOFL/wDtHw7Y3RPzPEA/+8OD+oruovRo+r4Xr/xKD9V+T/Q9K8PXG+xeA9Ym4+h/yaW4/dTtGBwDkCsvQ5/J1JUJ+WUbT9e1dHcQxtIsrLk9K6qcrPU+0qxcloZ5DSEZi69MitK3iaNfmIJ9qAwXABoO9z3xVSk5aEwgou5KzIBkkZrLukuJR84xHn0q0QsTZVTnvmkBDtmRuOwNOHu6oU/e0ZFbWltHHvfLMR3qOR0juA1vGTjrirxkhPBwPeqqY8w7AT9KpSbbbIcUklEPOml6IQOc0VIZVVsE4GDRR6ITXdlO2EsJ3GIDpzitJXUgHPNUd7k7G4OBwakRDnOcU5q+rHB2VkSNbWzy+bIu5vepC6gYXp7Vn3sk0fP8PrmqqXbg1SpSkr3JdSMXaxdu4JJ4SY1JYdCDVWHTJnAMs6p6r1NTLelxt4AoDTBt4+Ye1WnOKsQ1CT5iwumgAKJSV78YqO40eIrujdlYep61Ml8gUbjzTzcRTEEyYrJSqJ3NXGk1YrJpSC0YSYZ+o2msFlZXIK4wa6xJYSuzdkVn3EdssgKxitaVWSbUjKrRjZcpkCKQRh2QhT0NKkZkbaOSa1ZZEaHbgYrNjcRTZNbxm5JnPKCi0Wxo7FN5kO7GcAU2HEB2tgn2q/8AbkWDcCCcVjzTmWYvnrWcHOd1I1nyQs4k1w0TnOBmqj4IwvFSYUr2qSONNpJ5rVe6jJ3kyqCynOamSZh15pwh3nirkVnCuGfn8aJTitwjCXQrozycKpP0oDMK11kgQYSNV+lQ/YlclixAPPFYe0XVHR7N9GR21xtPJq219xhQST0qKO0hDfNlvxxVjy7eCRSvX0PNYycW9jaCkluSQu5HzqefWq80OHJVSc1YcSOR5ZwPrSqjqn7wj6isk7O5o1dWZUhQhxvBB9K0VK4rOkB83KsTViKEgbm705q+rFB20RJOFwGKgkd6r+dwcCrnlqVwRmomhTP3KmLWzLlF7orKwzk8mrMZ44pkkC7dyr+VSKNicLTk00TFNPUcrMxwwxUUsaDkcGlh+d2LHn0qWRd0ZGMntU7Mr4kU/MIOAatRHIrP2ytJsCkmpVS8RsKv5nitJRVtzOMn2NCjIxkmqnk3TjLyhfpUbWkx4a5+X0FZ8q7mvO+xbe4iUZ3g1Va6JPFC2SqMvIWHtS4t04IH0q0orbUhuT30FS4BIBq0rqRmqTeT0UYqWI/3eaUorcIyexO5AGaYr5bFJIcLyKh34OMVKjdFN6lzODjtTHGTkdaUKGUZpjRlWyHP0qUUxwYL1qGfyn++ivj1p/l5OSwNPMKlcEVSaTuS02rEKJbyJtaJMdOmKrTaaq/NECwJ6dxUzRlH56VPFJk4zV8zjrFkcsZaSRleVEhwV5HqKcHVB8qD8q1Xjik+8oJ9RVaW1K8pyv61aqJ7mbpOOxTW4cH5hkU5pI26g0/C+lMOOwFXoZ69WNMUco4GKjNvt96mDNnCip/Im2ZKZz2zRzNdQ5FLZFHyVPtR5QXpUzbg2GQg+4phzV8zM+VDdh9aTyvxp2KOexouKyE8oHqMUnkx59acA1TRW8svIHHqaHK27KUbvREkdz5MYREUAVHNKk2N8SkjocVcSyhVRvUsfc0k1ijr+6wjD8qwU4XOhwny2MeSIE5UfhSIsaD5o9xqw6PG5RuoqMoTXUpaHG42dwWXb0jUe+KaWDN3b9KCtKY2252N7HFGgtSeKGzIG+Ln1zmrMqrIgVZNorMG9W7j61NvbH3iazcXe9zWM1a1hvmtbuVznHpTvtPmfeYrUL4JzTCK15UzPma2Bzh+W3Gnwp5hycn3qLFWbd9nTP405aLQUbN6lxGEKbRyKZjzDkqDSq24j5RTpJAqHBA+lc/U6enkJ5SbcED6GmmJVHyDFVftB3c8ipfPDDjiq5ZIlSiyYFgMHmkKZPeoRIQ24kVMtwppNNFJrqOWHbyTUw9FGaiVnc/JGTUgE4P+rP41D8y15EyqM/MOalCoOQADVVmkUdOaYTI3es+VvqacyRbcgrg81CkQjO5RSxhvQmsDXvHPhfw4jjVNYgWZRn7PEfMlPttXp+OKuFOc3yU1d+RFSrThHnqNJeZ0W/nHSmkEZJyB6mvmTxT8YfEutXMkWjTPo2n5wqxY85x6s/b6D9a4N9V1a5kIlv7yd5DyDKzFj+fNfRUeH60o81SSj+J8tiOJaMJctKDl57f5n1z4i8W+H/CtiLvWdSjhDDMcSfPJL/uqOv16VX07xel7oEetT6NfWdrInmLlRI2M4AKryCeuMV4no/wl8TSWMevXgihuYFSe2tJwZGfBztcfwjHOOa7rw78SrmKLUtP8aJFY3li2HnRgm4E4C7B1IGenb3rCrl9KMLUH7Rp62dvuX66nVQzKrOoniY+yTWl1+b6elkd5oniKHxClxNptuWt4WCh3kAckjunVfx61p+dNtIMDK4xlcj9D3riNP0nUNVsIv+EbuW0XTGkLtPJa7JZ4yc7UwflX/aPzfSt3VfEFj4URm17V7a2tWUCAsS0rsPvDH8X8682pSjz8tLV9tbnsU6j5Oaq7LvpYs3V5rbCSKysY4H25Sa6JZM/7q8/rTLae+km3S3EckZx8phKY47HPr603S/Fmj63bJc2kyyQPzvEi5QdiwzlavzJFcoWALJjJIPJHqMdR9Kl3h7ko2LiozXPGV0Yt/rrxRMNNtTd3PIRWyisR16jNcnceI/E+m6i11q+t2VvagjbapCGBz0BPXOeOCTXU50uxRojNJbsTtWWVGcyE9w3OR/Kuf1eDwrpInv8AWpA+4nIkYuxx2Rfr1Ir0MP7O/Ly3v5XbPPxKnbmUrW87JepRsvjBpk18ltf6bcQxu/lpcxDKFgcHKnkYNd5Hq2myusf2qOOVuiSnY3614WmpeHbjUbkpkWFxGQkSjYVJ4Ibg56A568CrnhLxlZIbbw94mud9tvMdtesvIUHCpJnnafXt3rur5fFrmpRatv8A1+a+48rDZk1LkrSTvs/8/Xo/vPeEhLqGXBU9wcipPIArlpNIld2OlX01lLG22TO5Awx1XsfasbWIH0Kw/tHUfE93aCFvlc3LEyHsCv8AF9B19q8iNFTdlLX0PcnUcE5OOi63O6upbKzVWvLqG2DnC+a4XcfQZ60/AUYAxXiXhf7f42+INrqlxLK1jZM0zvcMC82Pujb0RdxHA/Wvbjz71jjqH1eUabd3a78jTL8QsTGVSMbK9l5hS0lFeaeoL1o6UlLQAUUUlABRRS0wEqlq8epz6FqEOjXCW+pPbyLayuAVSXadhOeMZxV2lAJOFGTQI+PvCfxW1fwv4+v4vib4ah1nUC/l3MsljF9ugkUbVCnADLwOPTkGqvxX1K2v/jr4f8R29tLaxX1tpl3HFMuySNdwwGHYjFdX8evGeu3XxJ0zwV4Ou3t7xVSO4ltMLLJPIwCRlx83yrg4zxurC+N/hTXoPiZ4bs9G0vUNZnstGtE3xQvM0jROwJYgHqRnn1q0czvZrc0f2r9//Ca6BcD+LTplz/uyk/1rB/aJXzfFvh69x/x9eHrd8+v3v8a2fHPhz4v/ABNiPiTxV4Vt9DsdJtZXWJWCyeX959qliztgcA4Fc74sfxT8ZdX0NPCXgbU4bPTLBNOilnBIdR0eSTARR9Ce9CFLW/mfXXheX7R4L0KfOfM0+3bP/bJa2KyvDely6J4T0jRp5hPLYWkVs8ijhyiAEj2yK1cVmdS2CloooGFFFFAFqybEzL6ir55rMtm23CH1OK0z0NAHyd8YNOGn/FPUyq4S7WO5HvuXB/VTXBIdsgb3r239oHTgmo6FqwX/AFsUlsx/3SGH/oRrxLbXnVFyzZ+X5pS9ni6kfO/36/qXRXqfww1DfYXenM3MEgkUf7Ldf1H615WpJQH2rqPAl/8AYfF1ujHCXSmE/U8j9RW9J2kc2T1/q+Ng3s3Z/PT87HvcTsjK6nDKQQa7lWW5tEkXkOoYVwMDhkBrrtAuPN08wk5aFsfgeR/Wux6H6xujRG1AMClLjjBwfSqs7SRysB06iqUksjHk8ito0+bU55VOXQ0JNpbc7AYqrPNEeEwPeqxZm65P1ppQmtYwtuYSqX2QxpWz1zUyXpSPYq496b5Q9KUQ5rV8r3M1zLVCG5LdV7GipUhUH8D/ACopXiFpPcry3W5g2ewqaG7U8E81nFOfwFJtIPFaOnFqxmqsk7mu3kTDMhyaz7i3VMmOQMPSogWpSAwwQaIxceo5TUlsV9xU9anivni7ZpDATyDTfs7CtXyvcxXMtUSTXwkXGzDetQC4YHipBbnPSrMelyyruBQD3NK8Ioq1Sb0K63jg9cUrXbMMZqx/ZTBtruFP507+x3Ayrhvao56RfJVKPnuON3FQO+45J5rUXSZicbfzNDaTKv8AAT9DmqVWmupPsqj6GVvbGNxpNx9avPZ+XjejL9RTfs8fqa054mbhLqVRIakEpqwtop+7mnfYTnAzn6VLnEahIjjnxU/2gleDTPshU4JwfenC1ftg/jUNxZolNCLMd2c1pRPK0Y3MFFURbMvUD86sC2fbwB/31WU+Vm0HJDp5ZEwAR+FTW+XwzdagFsw5OD/wKjzSnyBwlZtXVkaJ2d2aLzHeFBApWZimV5NZQO58+d+NW43Cj/W7qzdOxoqlydIyTuc4qysibcDpWe9yo6c/WmrK7nAbFS4N7lKaWxc8/wCcqDxUjSgL1qvHCgG5zuNBkgDYJqXFX0LUn1JFk46U9mCp1GaqyyRBcq3NVGmbBAOatU+bUh1OUsCfE3JrQjcOue9YiBmbirsXm9ATTqQRNObLrfISVGWPrVecyDB381IsbdS2T9aHhkccEYrJWTNXdoqNO4Xr+NM+0uOtSvazDoMj2qEwTjjyzW65WYvmQ83RPGaaJocZcVEba4b/AJZGo2tLjvE1Uow7mfNLsWftFvjpU0N7CvGMCs77HcdfLak+zzD+Aim4QfUFUmtbGnNfx4wnJqotwS2ScVXFvMf4DUqWkmfm4oUIRQOc5M0Yp8jGTUsmNvGcmoYY4415BJqcBWOcCuaVr6HTG9rMjRXByTU4fnBpCQBio1yGzUvUpaFhsEYI4quQqHI5NK5ZuQaFBIIwM+9CVht3GhnJznipVbIx1oVRjGKCp7cCh2YK414I5F4wp9RVR4JEGSuR6irgDYwDxUgBA5pqbiTKCkZablbI4xV6GYMPnNEsWW3BRUGBnpg1TakiEnBk88fnqpUgEetV/syJ9+QE+mKmCs4xnC1A6ANwTiiN9rhJJ62LMa264wgB96kaKJxygP0qkW2rwOalimzjc2KTi90VGS2aFW0jEhJO5ewqxhVXCjAFN8xSODSgqVNQ23uXFJbDs5HFQzeftymMCgyBDjdmnefGByaEmnsDafUzJBLNJgKSw6ikWCQtt2kH0PFXGljEm9Uyx702acOQR1FdCk9rHM4R3bIoVCSfOnPuOlWvOUNyPxqo8jEZIqNXbPPShx5tWCly6I0HMbDJUN9az5YsuSoxUjSkjHamblJ64oinEU2pEDRHHSmmPFWcKxyOnvUoUbPuZrTnsZclzOKYpNprQESk5ZePQUxhCOApz71XOJ0yspcA80h3GpgQOwoYluwp3FbzKpUmjac9anwe9G0VVybEQUnrU8SgHkim7aJClvA1xM6RQoMtJIwVVHuTwKlu+g1pqXkl2jrVLWfEujeH7D7brV/FZw/w7z8zn0VRyx+leX+K/jRo+lI9l4cCapfHK/aCD5ER9fVz9OPevGrltd8V6p9rupJ7+8mbaHc/jgDoAPbgV62EyaVb36/ux/F/5fM8fG57Ch+7w655fgv8/kep678drhroR+HNIiW3VuZb3JaQeyqfl/MmqJ+K/jKe1F5PPpmm20g/diGHzJXOSOFZj0xyTiuQufB93pOjz3ssbzTxYz8h2r3JAI5HX5ulc9ZapLppnkjgRrh1xHKyhjEehIyPTP489q+hpYDBuH7mCdu+v5/5HzNXMsdGp+/m1fWy0/L/AD+Z2HifxrrNwosZ/FN/dztlZkVlhhjOePufeBFcXcwTxXXlMVdmAYFc/OD3qgdjDLHHua9S8E6Pd+Hoj4l1rU4NIhEYaCK5VTJOvbII3bMdhy1dcuTB0/dS9Nrv5L9Diip5hVtO/rvZed3+pHoXw+GoeDTqMuo2CXt5IogV5DmGNT85OP4jwNvpVrwjbTeH/GmjW2rWVrC8rvLDceXvZs/Ko4+7yM98Z7ZqnqXjjw/HdTSadpDzTkMyXMn7kNIf+Wh5J45wABXHT+MteeJoILtrOFmLKkBIKZGCAxywBHXnmuNQxNdSjPRPvpa+mn/BR6LngsM4Sp6yj21vZ31v+j0PpLxH4g07wXohl1SdN+SYbZTmWdjz8o7DJ+8eK8G0jxJpFz4mu/EniiCWaUS+bDaQRqUJzkbieuOBjqe9cfc3U903n3dxLcSgBd8jliABwOearqJZXCpkk1WFy6NCEk5Xb3exljc2niakWopRjsnrr3Z9Eaf8TPFHiSWOPR9Oi0myZvLF1OPOJY8KMcAc9a8v8Wa/4l1HVprLWoo5NQgLq4W1CsVx94+vA4OOB3r0f4e2CQ+G5bltbtIdLhtgJbjcreVICGYEsONvPGO/GasatY2niTQ73xX8P7SC8vyWgmmePMpUDB8sNwpxzjHIOa8mlOhhq7UYJJaX7Pzb7nu1qVfFYaLnUbk9Wu68kux4amqTaWj20lm0V2u5dxJQpuAyCO/GevrXSeFPHeoeH7mFV1K6msd5P2eIHKH2U8HPpXM6ppeo2U/maqP9JkY+YHlDuGH97nI/Gr+iXOi21wDqdt5pUcdSA54XIHBXpnvXvVYU6lNuSvf5nzFGpWpVVGL5bd9PvPe9K8baX4nuoBpeqRQOY3E9jJmK5L9mjB4zx0yetc74qsrjVbOSeQXc06KZJLeVQk0a4ysbKM4JwSD0IzXmOueC9d0Oyh1a6FrFA53wyx3AJzwRgjoeRgda6Lwv8Tj5nk+Lo/tZeMW7Xmwb3i7LJj7+MnBOf61431JU37XCvmS+/wDr8T6NZh7Rexxq5W/u/r8DmLq7ng1WNdNkltjbKJBABu2bsZyT1yCM8AVL4kktr7WI7q3vUuNyBCEJKxEcYyMfXjjmu88Y6H4N1Gymu/Dd3btqj5lEFpIHWVOBtPOAe/r1GOK82uUk0KIW+r22xZ8MphkVyoyN3TPQdsjnFeph6tOqlPZ7We54mKo1qLlDeL1utv8AgHfeAfHWoLcQ+EtYU6zY3DeVHvYF4vxPDL9enY12fiHwtB4tt/7bubnztLtYX+zJbsQxI4z9eAMfhXkB8VeENOkjk0DQ7wzKAvn3DBHXghmDAk5OfoPStjwp4jvNcvLfQmnvoo5rneFsVVB8w2kMMgYOcnHua4K2HfM8RRXJ3/VnqYXFLkWFxD9p2/RX6ntXgvRbfSNHzFbLGz/uw5++6r0LHuck11FQ29tHZ2sVpCT5UKCNcnJwOOp61LXxFeq6tRzfU+9o0lSpqEVawUUtJWBsLSUuKMUAH40UUlABRRRTAKxfFuj3XiDwXrGh2N99gur62aCK5BYeUx/i+Xn8q2qgvrk2OlXl8IWnNtA83lJ96TapO0e5xigTPJfh18B9G8D6/D4j1LWZ9d1aDLQlovLiicjBfBJLNgnBJ4zXs3mMwIVjjuBXzJ4o/aC+Iel6VFqUPw3/ALDsLh/KiutTWR9z4zgDCgnAJpvgZPiR43vdE8c+JfiTbaXos12ssdkbvyvtASTBjWFdoAJUjknOe9U11ZlGUVpFH0tIqJG8srIkagszOQAo7kk8YrM03xN4f1m8uNO0jXrLUbi1QPLFbTCTy1JwCccDmvn3xJ8f9S8U2useGPCfgWe48+CeCZ5ZTLIkeCrvsQYAA9TivHPh9ceOrAa9d+BtSNk1pprXN9IrIrfZ0IJ27gecntzRy9xOqr2R99d6K8O/Z08Z+IfFmha/D4h1afU5LG4iMMlwQXVXViRnuMrxnpXuPPepasaxlzK4d6dSUUigo7UCigByna6t6HNbHWsWteM7oUb1AoA8x+OGnfbvh2btUy+n3UcufRTlD/6EPyr5jIxX2f4w03+1/BGtadjLT2kgUf7QGV/UCvi/OVBPeuKuveufB8Q0uXERqd1+X9IsRHMePQ1Yt5ntrmK5jOHicOv1BzVOE/OR6irAqYs+OneM7o+j9Mu0urSKeM5SVA6/QjNdNoE/l6n5Z+7KuPxHIry/4d3/ANq8NRQs2XtXMJ+nUfof0rvoJTDNHMp5Rg1ehe6ufsWErrEUIVV9pJnZX0ZISQfQ1S2Yq7fSStpNxNaRLNMIjJEhOA7YyBn3r5/1D4savdvbSWEUum3Sr+8iEokiLdwQR0x716mCwlXEpqn0OHMMZRwjTqddj3QJTtteQ2HxS1eSC3llm0eXzG2uku6FwfQEZBrqbP4gTtJFa6l4Q1e2u5DhREiyRMP73mEgAfWt6uX4inuvxOejmOHq/C/w/wAjtdtG2uci8feE2uTbXGoPZTL95LmFkCnvlsEfrit+HU9HuIllg1WzljYZDJOpB/WuOdKpD4otfI74VKc/hkn8yYLz07Gip4ZIJiRDNHIQDnYwOPyorByszdQuZphdjv2EjjnFIU4wU/StAzHOF44FKGboUFa+0fYy9mu5lGMjjYfyo8k4zjitgSLjDilzGeqij2r7B7BdzMjiTHzGlaEZ+WtAwwEk4/WmCGNTwSfxpe0K9l0Ka2z9W4FW4isS4GTQ8fy/LkH61FtcDqPzpN824Jcj0LX7qT5mwDShVXlX5qkrsp6Uu7f0YqanlL50WjKVPzHBpVuVJx1quI1fhnJPrQLc9S3HtRyx6hzS6FrzUPDEfjWfdxp5oZBgnqKtqluOuSfc0EKpJUqwP50RfK9AkuZWZBbFIx8xxVsiOQZjfDVEGjI2iPP4U1Nsb52ECh66gtFYr3VtcL+8ch19R2qKJHY4AJrQdy42g4BqNUZW2gDHrVqbtZmcoK90QGGTONhqMbweKvPDKwA3DFLHbMvzFhn0o9orah7N30M9nfGKjIJ/hrVMSswJ25+lMe3yeF/I01UQnTZl+UeopVSQHgkVoeQR2oEY7jmq9oR7IpFXPXmlCSj7tXvLFIVPQYpc5XsymTNjBY03Yxq8Is9acYQBuxx60udB7Nmf5LnvThEwqe6uLWxtnubyeO2gQZaSVgqj8TTrWWC/tUu7OVLiCQZWRDkGnzO3NbQSir8t9RsbFP4asLPjquKQwyDoh/KsaXxDpUepf2atz590Mb44B5nlAnALkcLyalRdT4VctzVO3M7G+tyvepFuUPesG61WwsVaXUpPsEAbb585Cxk5xgHPX2qzdXNvZWsl1cSiKCJdzytwoHrmpdLbTctVt9djYEqt3qK5vrSyjWS8uYrdHYIrSsFDMegGepryPW/iheC1kfQbWOOLcUS4uVJZsfxKvQj8/fFcNcaxeavcm41q6m1C825QAnZDxk5xwuOuBXq0cmqzXNUdl+J49fPKUHy01zP8D6bEgYfKQfpSZZh8pGK878Ha7D4z025d7Sexa1URPcxyFVnJHJx9PfPNbmjeHp/D0l1Lp95JcwXD72tpZNyqe5Q9ifevOqYZUnKE3aS6f8E9WlifbRjUpq8X1/4B0xRj1aomIzgknFOSdZEU4KsR91uo9qY+d3IIrmV+p0SAbfQ08BMc5qLJFLgnvTsTcczKDgcj3pN4FARSec1IIYz0zRoh6saHHoKXzF9cU4woO9RnavAUUtHsPVbjw654p4Peq+89gBRuc8Zo5Q5i2G4oYjb6VXVnHFO39jio5S+YergVIMk+1RBk9RSh1H8VDQJkuKgaMlySBigXKmQxg8jtUm7dQk0F1IAQRgUxoi3oKdwOlBc0a9A06kDWzHuKYbZgODVncTRVKTIcYsq+XKOMUu2QDmrJBrI1TxLomjzC3v71FuCu/wAhPmfb/eIHQe5xWkVKbtFXZEnCmrydkXSh70ojY1yP/C0vBhjmdb2VjEuWXysE84wM9T9K1NC8XaXr8cj2zeWqNtyZFcdM5OPu/jW88NXhHmlBpHPDE4epLlhNN+TNsRAfe5pGjTsMVnaj4h0nTZ0tpbtZLqTlLeL5nb+gHuTXAePfHuu6WsWnaPbxW99OQNrEPKgI4OOgP51VDDVa8lGK37ixGKpYeDnPp2PS3hx1JGelR7FH8VeL+CP+Eq0nX4H1DULm5lvIzLdWs82VRWOI5CCc5z6djXrtlqlneyPAr7bmIDzIj1UkZ49R7j0rbE4Z0JWUuZd0YYXErEQ5nHlfZlwBR70hVOwqUKp6UuAK47nZYg2nGQDTlLjpmpwQO1KZfRRS5n2DlXcrjez9TUhiGdz5aguTTec8Hk0ahoiVGt15MeT6YpfMiPCwjPQYFcXqnxD8G6PNNBda2ktxCSGht0Mrbh2yOM/jXlXir4wanq8L2GhQPpNm4KvMWzO49MjhR9OfevRw+WYiu9ItLu9F/wAH5HlYrOMLhovmkm+y1f8AwPmen638TfBuiX8lhcXslzcx5Dpax+YEP90tkDNZ8/xc8Gpp5uoPt9w4O0wrb7SD7sTj9a+bnOOgoXkEsTt9AetfTxyLDpK7bZ8hPiPFSb5UkvTb8T07WfjR4juJmXRrO106HoC6+dIffJ4H5VxF9rPizxXOPt95eaiM4w7YiU/ThRWIH2nO7nNdjpHiTSU8MXUXiCF9QuIHX+z4FkKjdyS0gHBQHHUZOcDjNd31elhop0Kav+P9fM4Y4qri5OOJqu33L+vkytptjpllculzLDNqLRkwMwDWycHJZj1PHBHGfWut0C+tJIVsdK1BLzVGi2xsV2GFccqpIzIVJJycD615pLb6pqsNxqdy7GGM5aWT5ULE8KvbPsPepdM1KXRfKvNO8yPVY3fFwSCqIVxhVI+9yeT7Yp1aDqRet3+F/wDIuhilQkrK0fxt5eZ7LqjyaN4ZuzM88jzH5nvrjyzcHgNjPzFDzwO1eafbPCsSQW120lyB+8uGso8GQ54iVn+6PVsc8cVz91fX1/L5t/dTXUnPzTSFyM/XpUcJMEqzxsUmQhkYfwn1ooYR04u8tX20JxOYxrTVo6LvqaF3qgTWDe6XYQ6S0ZPlxoC5j7cl88++PyrJup7u7u2uru5luJ26ySuWY/iaUglixJZickk5JpwUAZf8q7VGMeh5sqsnfXR/1tsLZWN5qExit1BCjc7uwVI17szHgCti5h8PwWsGmWVwst2WZrnUpEby+nCRDrt9yMk+grFJYqVyQvpTCOck8VEouTvcqFVRVuXViyReXKyFkcD+JTkGtu11PwzZeG7i0fSJr/VrjgXEknlx2vP8AHLEjqTisQnPAoaNMcdfWpnHmSTf6ChV5G2lv8/zNK+8Sa1q2mx6GbkJpwdTFZQoEjVugwB9e55PWo1uvE/hW8udMF3faVMSvnQq5jJI5BP+NZQURyq6YLKQwyMjNdzrHiO38X6SlxrqMmqW4VEvIRkY7q6+ncY6ZOPSsJxUbJRXK9/X9TupT9onJ1Gprb06q/Ty6GXBZ33iu7+2ahrqSSJGfNlu5RujA6dSNw57ZI9Kw5lazmcRlJQjEb1GVOO4NU/MkWTpgqcgj+dac+qT6jefarx1a4ICs6KE3Adzgcn3q1GUdtuxnKcZK8vi773GS+JdSm0+LTbi4eeyiO5IHOVU88j35PNWJNJgutJXUtKvfOWNVW4hlG2SNznoBwV9/wA6zri1gxujIDHsKfpur6hpIu4LWYxRXUfkzpgESJnOOf5ihR09zQpTjL+Jr+a7EJiliGDkZq9b38xKC4P2hY8YikGVI/pSzSQ3LeZAvlhVB2k7j7nPrVeHy2kJUMjD8qUtVqYKUo7M0oLXRrlbpYVMN2QDBFKcqx3cjPqB69a7v4O+H7i98eT6vexuI9JiwqsMASMNqgD2G4/lXnc0QkPmIwDjkFTX0p8LtHk0j4e2L3O77VfZupN5yQDwg/BQPzrx81xLpYVpby0/z/A+gyKh9YxUW1pHV9n2/E7YUUUV8EfpYUUtFMBKWkopAKDSUtFACUUtFACVn64mrS+HdSh0OZYNUktpFtJWOBHKVOxjwehx2NaH0pHmS3ie4mbbFEpkdj2UDJP5CmJnwh8RtQ8SLctpPi/x+PEWpWdyY5rCNpXS2cDBbcyqme3y+tW/g/oHgXxN8QtC0zWn1ZNSecywyRSRLArx/vEXBBY5289Oa53U4j4msvFXjSXPzalGVPvO8jY/75UVPaxzeELDwL4+09D5kd9P5hH8bwTK2PxR8Vr0OJb3O/8AgvbSW/x98T2HQT2upw4/4HWF8EC51HxnYSDIm8L3yY/3Qpq7o8XxJsfi5q3jDwD4QvdSgvLm5a0uJbUmCSGZiQwJKg8Ec5rmfh7p/ijWPHM+geFtQTStXu7e5gleU7VKAZkjJwcZxjikVezR63+yjOrzeK4QesVrJj8XFfTleRfBb4R33w1XVb3VtTt7m81COOIQ2uTHEiknliBkkn0wAK9eqJas6KaajZiUUUtSaBRmjvRQAVpWjZtgPQkVm4q9Yt8rr6EGgCy4DEA8g8GvibxFp7aT4q1bS2GPst3JGPoGOP0xX22eBn0r5V+M+mmx+J93OF2pfQx3C+5xtP6rXPXXu3Pl+IqV6MKnZ/n/AMMedq211NXR7VQIq7Gcop9q5YM/P6y2Z3vw1vjDq91Ys3yzxiRR/tKef0P6V7FEdyAmvnbQb/8As3xDY3mcKkoD/wC6eD+hr6FtX+TGa9Ck7xPvuGsRz4V0nvF/g9f8zttGlE2lxA/eiOw/h0rwrxv4NstH1q8mt7kRQSzGWVXXCxRvk5z6A5r2Pw9Pi4ltm6Ou4fUVwfxr0q3/ALOs9Y8qMMwNlLM2dyqfmXGPcHr617eUVXDEqCdlLQ785ownhnUkr8up893N3bG8lFq7tDuIQyYDEdiRWrpPi7X9Gm83T719pUK0bncrKOxB4rnbuz8pjz8meHHINVd89uAy5ZD61+iOMZx5Zq6PzCLlCXPSdmeuWPxL024mih13Qg8Lblme2Hlgq3HKc579CK15rrw3PEkukz2kFhEo3mNNoBJOFKkbt3JxlhnB9K8TjvVfg8euanF8qRFPPOw8lFJwT71ySwFK6dNtfl9x6Ec0xCvGpFS+Wv3nqV7e+GNK2Xejard/ayCcWrnKv/eO7oPYE0V5YurLExEH3iuM4orSOFp2973vN2/yMZYvESd4rl8ldfqfbJPPTsKA59KkYDP4Cm4r80P1HUTf6qKbkehp2PajbTDUaW496AW7HFOwPSjFAAXfGDg03cx7ClIoxRoF2Advaglj0wKXFG00aBqIGIPPNODj0pNtJto0C7Blycgik2sBjAp2KTB9aLgPjDJz/WnFmYYY8VFg+po289aVirkyqAcgkmnbk7g5qDDHgZNKEk/uk/hSaGpE4dcdTSh1IwTn6VQubq1s0Z7u5ht1UZJlkCYHrya4vVfiv4O0wskF3Jqcw/htEyv/AH2cD8s1tSwtWs7UotnPWxtHDq9aaXqz0P5OoNRzTRW8JnnmjgiXq8jhVH4mvCtW+NWpzq0eiaZDYqeks581/wAuFH615zqmt6vrlz5+r6lPeuOnmvlV+g6D8BXt4fIK89ar5V97/r5nzmK4ow1LSgnN/cv8/wAD6ntfEmgX8jx2eu2Fw6dVS4XI/WsjXPiP4Q0BGFzqi3VwOBb2f71yfw4H4mvm62vhacwwQlypUvKgk6+meB9RVJ2jLBtoGOlejDh6lzXlJ2PJqcV1eRKEFzfOx63qHxm1e5Z003TLfS4yCUkucySEfTgZ/A0zT/i14lD+TOmn3eed8qeVgfUEV5VGY5pUSWYRKTgyMCQo9SBz+VdZ4bn8IaZDf/25pUutzPH+4ZXaNA2eg6EAjB3deOld1XLsLSp2VK/pv97/AMzz6ObYytVTlW5V5uy+5L9DpL74n+KZnka31KysI15UQQbyT6Etnj37+lZFr4g8YX+v2uuXN9cTRwSBh9oKrEwPBAUkLgjPPbrWRNLo8c+/T9MnEYyR9qn3HJ7HaAMDt39TVG5uVChZXIVRtWNDjA9Mdq0p4SlFWhTSv3Sv+H+Y6mNrSanVqN2d9G7X+f8Ake63vjLwC1qmo3epQyFiVMCoZmBHUbOmPQ4waff/ABJ0DRtNeWG/imuGjDw2aAF/mGUUheB788V86T3Al2KsKRqox8vVvcnvViyigVfMGUuFbch7cf1zjmuL+w6NlzSb8tDt/wBZq8m1CMV56/16Hpeh+PdXurrUr/xfcvc2DR+X9iiTYEY/d28jHf1NaeneJNF0TQ2vfCtpDDeX6/PDJkhJVbBMjsQMEZwAM815ZeX8k9zJcTSeZJLguT8xY9yTVK4vpZXMshxzklvX+QrpeWU5u9rJ20W2nS3X1OSOd1KUbX5mr2b31631tp02N/XNWvNfvPtfiGSS42EiMI20Jnso6YHsPqaU+JdRe0it9Ru5dRtoOILaZiY1IGASP4sdga55Lk8SFt/oQelIZg8gLHAz9a71hqdlG2i28vTseU8wqXclJ3e/n69zYjvJluMzDz3AwinlR6cD09Ks28P2mKdPtCwrKAJpyMyMc8xxAdSeMn88CqkWn30unvqMVpPJaI2wyxISob046U99I1z+yor4adcrZybkSZUJUAdRgdPxqJOF7Jpf5nVBztzOLa3+X9dT3/wHd+Hp/DltYaHIbb7MpD2ckoaQc8s3rk85FdJcwpJG6K8qyOOWhfY+P7ynoTXzd4ejh8Oa5YXt1qMltdq6uvkxmWXyyOSEHUY67j07Gvd9H8ZeFdaE0mma9bMIgJJEl/d7B0yQ2P0r4nMcHKlVdSm3KL1vZ9+5+g5Zj41qSp1UoyWlrra3Y1NL1OxvkniW+iujA3lyErskQ9MOp/n0NJdte6UHmsomvLcDItgcnOexJ46/Tisq5uvCx1QTprWmi6iQlyLhVYRnk/MDyO+DmqieMtFHi9NHi8QwySTIgig8ksjs3IKzLxyOx71wqjJvmhFtWu00/wBPz0PSeIgklOSTvZNNfr+Wp0FrrFlqhezbz9PvQvMMq7HHup5DD3Gadd3S6XEJbp2NqoAaZuqdsnA5+tXRaWU4WfakndXByPqD/UVAUu4nRIZEmtQSHjnzvx/st3HsR+Ncycb6bdv+CdPLK2u/climimjEsEqTIejI2Qafv9Tise50GK6t/wDiT3E2h3KZ2PAoUZyT8yHhlySfxpFvNc0+aUarp63NjHChF1a/NI0nRsxjtnnPar5FL4X8tn/kyHNxdpL57r/NG1nPek4qksJvES9t7xhBIMqhj2kfnzVa9uNUt/LFrp/2sMSGYShQo7ds80lC7sn+gSm4q7Wn3mmevFFZI1G+lsmNrYf6WUJVJnwgP+0Rz+lZNzrHiaysoITYpeXLShJ7mGIhIVPO5U6vjoTwK0jQlJ2TX3mM68Y6tP7v6/rc6z8aaVP94mud03WLxVMeoI0yKf8Aj5GBnngEDv8AStW91aw03T2v7+cWtqnWSX5R9B6n2FKVGcZcu/oUqsZR5novMuYpQSONtcPpHjq78WXt/beFNOiaG0ADXl85UEnpiMfMR16kdO1ac2iavqETRar4nvUikUK6WqJCp9duBuH5mtZ4eVOXLVfK+3X7l+plTxCqx5qK5l36fe/0uaVzr+i2morp81/F9tI3eQh3SAepA6fjS3Q12/t1bTJ4dPA+bMi+Y7+3ooPryajtdJ0zS7cQ6Tborquwu4LMR7k8t+dVb83tzayNBfNZBVBeaYhIoxnk+5xn9OaSULrk/H/L/hzS07Pn/D/P/hjQi1WS3smm1KSNHgH78KQSnfsSK4vV/jFYaNcxrc+H7xrWXIjnWRfmI6jHbjBxnoRXn/irxJpuneL7Gx026u5dIsyyXUqy8XjNyxU9Djj5vXPoK5iXUtIl+0Ravf3OoQXiLIVhHFo4Bxt3dSOF9MfhXvYfKYSSnUi2nr1X9d9fTc+bxedSi3ToySadruz/AD6dNPXY+h/D3xC8P+J4v+JbcJ9o/wCfaRtso/DHP4ZrjvGHxdudF1g6RoNhbajdRErO7FiiN/dGPvEd+1ea+ENdt/BFwLn7NDJe6gU8mS9iZFtockFyRycj+76da39W8UQ+Ibk2WtCPSRIpNtf6e+2KcZ6ZbGA3TJPynGcZNCy2lSrt8jlDzf8ATdiXmtWthklNRqX6L+kmzbsvipr+p2Q0s/ZbbXJX2AhwFVSONoPG/ttJ/PpTpfBkMqhtXmu5Lx4y1w8RJaV8Z5ByWb/63ArjjpOh+A/K1fUbeefVywktNPnlU7V/56y7M8HsM89a6/wf46uL3xddeGPFFlGLqViY7q2XZ5eBnDZ6rg8N19c1tUp+yi6mEVo7vu/TyX9XM6NX2slSxzvLZdlfa/m/6sJpXhrSdPhe71jSIrSzSPAWaf8AfbO7yv8AdU+ijntWJJ4l0uPUprDwbYIsk6GJLgQF5HkJ+VUXP3fc9+TXd+MfAV74knhA8R2lppkMYKL5eWZxyWbBAxjp6VLoHgSDwvDNqegWkOq6m0IW3ubiYBCxHJGB8q/TJNc8cZRcOepLmk+mtl63OqWCxCqezpRUYL7WnM/RL/gHParoo8D6FY+KtZ1ae48QFCBFw/mznkBj/cQdQOprktE8RJpwvPFWqN5199oCSSz7XaSRhnai9gF5J+gGK9PTwDfeIfCE0HjGea21Rp2nE4uVlVGIAyoAACccL+tQWfwg8MwJa2+oTXWoC1yfL/1SyueSzY5PYdegqqeNw6hKNaXNK+tl07LyIqYHFyqRlQjaNtLvW+131v8A11OJ8PvrE2s3njm4/wBHsrjK+fcgyO0ZOAQi9QMDngcV1mueN/C1np9vNc2lw1+48yO2aExyRyKepfPyjPQjOQa7O+s9Nj8NTWzW1vZWIUQuiyKkSjoVLH8qwvEPhzRNe0GG61rylW1jKpeIfKKgdPvdeABg8GuZ4mlXqKVSLSTtp26f19x2LC18PRlCjJOTV9e/V/5fiLpnxEhl8m5eEvZXC7oldDHMFHDEE/LIAc8A5wOM1tWurHVPE0wsb95IbJTFdWyhdoLcq2PvZHQ+ua+f9b8UxXluk2mMLFrUtapbhdvmQ9VYqMr1LZGccjArA0LxPqvh/WU1WwmK3EaMgyTg5GPmH8QHXB44Fd/9i88XOGjton/XbqjypcQKnOMJ+8rq7X9d+j9D69jAkXAY7vpxWRr/AIi0jwxZ/atavUgB+5Gp3SSH/ZXqf5V80ax4/wDFuu3CTXut3CeX91LZvJVfXhcfrXP3FxcXk7XN3cy3EzH5pJXLMfxNRRyCV0609Oy/z/4BGI4op8rVCm79G/8AL/gn0j/wtvwOIWl/tC5JH/LP7K24/Tt+tea+Mvi7fa3by6bodu+l2UgKSTM2Z5F9MjhB9OfevNTsCZz8xPStv/hHTpelw6z4ihkhgmfbBZZ2T3GByxyPkQcckZPb1r06eWYTDyUtW+if+Wh5E83x2Mg4K0V1a0083r+G5zyAhcqvy9M1Ys4Y7iZhPdxWkSDLO4LHrjAUck06a5e9njGxIo0G1IYl2qo/qfUnk96rTMEmCAHPuK9m7atszwbJSutUSXDQiYJbqXVeDI3RznqB2HtTYLW5vrlLWzgluJ5DhIokLMx9gKmZbNLZCUlefJ3AthcY4x3zmlOp3a3HnWe2wO3YBaZj4xg85yc9+aTcrWX4lLkbvJ/JHReJvDFt4U0nTbOW4gutZvQ0tz5codbdAQFT2Oc5PtXKRR2iXsaXryC3DYla3AZsd9ueCf0qMI5OcdepqRY8ckbjUU4OMLTld9zStXhKfNCNlpp6f59TT1rXLjVvs9pFvt9JsV8uztCxIjX+8fVz1J9+OKy1TPWpREOp5NKMjgCqhGMFyxOerXlVlzSeo3aFGaYck4A4qUoScmlwqjFVcyuQ/dPAyaTaScscn3pzN6cU0kZ60XKQ1mA46mmbSTk047c5JxTlKY45pF3sCpxhV605IiZQpIBJx9KQuccn8Kj807uDUsFc3Lm3s2tYWKAvHlSYxjd6cVljAYjAKE4YdM1LbXKhjFJnY3f+771HN8x+UFx6isorl0ZtOak1JKxWaMB8dRnp6VHtAYYGfrVyOF5HwARkdM097aVYy2M9fwpuSQ0pNXRnygEAgnIP509YEdd0zheOo5pxjUrlWBbuPSoTIsQwwJPpTv2HrsSLLAh8u2id2J/1jH+lLLsUFnwrHt6VX8yeXiMhFP8ACgwamSwlYKScxnqfSspNRerNLczNLwtZya/4q07Qodzi6nVW44VByx9sKDX2EqJGixxLtjQBVUdgBgCvDfgf4biXU9S8RswkEC/ZYeD8rNyx59gB+Ne518TneIVSuqcdo/m/6R+i8PYX2WGdV7y/Jf0xRRQKK8E+mFpKWigBKKKKAD60UUUAFFFFABWJ4u0e68Q+CNa0GyultLnULR7eOds4jLDGTjnGM9K26QlUUuzBVUZJJwAPUmmJnhV98Avs3wZfwToeqQSapc6jFf3V9cqY0fYCuFVckAA4A+tdF4N+Dei6X8PrHwt4ygtPERs76W+jO11jRnAGMZBPA78e1ej2WtaJqN1LaafrFjeXEKh5Ire4SRkUnAJCk4Ga+e/Hnxl8ZaJ8aJ/CWn3lnb6XBf28OBaq0jxvsLAsc/3j0xVK7Mmox1Po2ytLaws7ey0+1jt7W2QJDDEuEjUdAAOgrltB+G/gjwvrcms6N4ehttTcuWumd3kG85fBYnGc9hXj/wC09q+saPd+GotK1a/sLe4W5WVLadoxIVZMbtpGcAmur8H+MF8L/ss6Z4tuEa+ms7NsRzSkGaQzsigscnknrRZ2K5lez6HsXeivMfhB8Rdb+I9jrWpanpVnp1rZzxwQLbszFmKlm3FuuBt6Ada9PqWrFppq6Eo70tFAwoopKQCirVicTMPUVVqa1O25Q+vFAGn1Brwj9oPTcJoOrqOhktXP1AZf5NXvFebfGjTTf/C++nVcvYyx3I9gG2t+jGs6ivFnl5rS9rg6kfK/3anyzmrEJymPQ1VzU1u3zkeorz47n5dUV4lnGRXvnhXUf7Q8O2N0TlmjCv8A7w4P8q8DBr0z4aagWtrzTnb/AFTiVB7Nwf1A/Ou2i9bHtcOYj2eLdN7TX4rX/M9c0+48i+glzgKwz9Dwa0vHuiLr3gTVtPAzKYjLF7OnzDH5Y/GsCJty13VjKLrToJmwflw31HBrspzdOanHdO5+i1acatOVOWzVvvPit9R0+EFXcXJI5RBkj8egrMvZ/tbqbezW2XqQrZZvrXR+N9Ch8M+OtX0qGAxwRzl4V28bG+Zf0OPwrAVDNnakhI7BCc1+oU6ilBVFs1c/Iq16c5U0tU7fcZrRFTmT8jSeW5GFGB9K2l0u9kPy25Hu/wAo/WrcOhTEEyzAYHSPn9aJVYrdkRVSWyOZ8th/CVPsMZorrjoxWMv5ZRAPvE5P8qKzVePcrlqdj1XTf2gLD+0pLbXdORI0AXdZuWZW753cEfQ1o6l8aba4Bi8L2MUzMCEmuZQT9RGPT3Nc23wv8D+aXgsbmFiOsd039c1nN8GvCnnCe3vNUt3ByCs6tj81r8cjxtkXtFNwkl2advwkz7p4TM3TdNVV66X/AC/4JpWvxT8awy/vLi3ulHVZrdR+q4r0Lw98UNH1WeKz1SE6VdScBnbMLH03dV/H864BPAUUNq0Meu3jMfuySxxuyfoM/jVCH4ezRFhN4nkvATnE9uuR7fKRXs1OMeF8TaPM4N9VCSt9ysebh8JneGfNzc67Np3+b1X3n0cFJGQMj1FBQhS5GFHJY9BXzOml+J9MvGfTPH81kCQfKWB0U46ZG7n8az9e07xnrV69xJ41juGkBDrMZApz2CrwB7VyxzbKJPTFK3+GV/xX6ntfXK9v4Ov+JW+//gH1FDcW1yGNtcwzhTtYxSB9p9DjpU20ntXyn4Y0Lxf4VM8lhrtoJJxh2S4ZAcHIyCOfxrtbXxD4rSKM3/kXkgb5vK1AJx/X6cV0xxuWTV44qHo2k/xdvxJjj6qfLOjK/lqv8/wPdyuAScADqTRsYZ+U8V8r6xbeOdXuXSUXU1srb41+1o4zn0DYrqPDGr+JNOJtvE/hbU9YtcfLILtiyD0CbtprsbwDhzQxcJPspR/WSM4ZnUc+WdCUV3af6Jnuc2qaXb3MdrPqdpHcSnCRtMoZj9M1PNPFboXlbAHYDJP4CvnvxXKuuXVvdaf4L1LTZYF2bmfgjOeVUf1pdN8T+NdDsxHCs5t1JIW4gaXaMdATzgeldsMJhqkFKnXjfqnKP6NnNLOnTqyhUpPl6NJ/k0j3e01WG7lkjNtcW2xtqtMoAk9xgn9cVobWzwCfpXzTc/GvxJF+7Fo0Tn5fN/s5hz688Cuc1j4z+KNv2OXVb+UEYbygIxz6lQCar6gpe97SKXq3+SL/ALZivdVOcn6Jfmz6xu72z0+IzX93BaRjq00gQfqa43WPit4P0pSsF2+qT9o7Rcj8XOB/OvmYeJ7HUIfOupZVmKk7ZG3MxHbn/GsxfECTwSPbW5RxjG9ga9SjlWFWtSbl6K3+Z5OIzzGtWpU1H1d/8vyZ6x4m+JviHxErW1uf7KsT/wAsbdjvf/efqfoMCuVh1XVbYMsGqXkIb72ydlz+Rrm9K11G1P7FqYEeVyXAA2d8n1Fb1vd+H7u58kassAA5kkjITPpnr+le1Qq4OnD2cI8vlb/hz5jEU8fXn7ScnJ97/wDDWIp2luZDJPK80h6s7FifxNRrCxPArXgh0CTUI7P/AISSFTI+wSC3coD6k9gPXFdBZaD4XlvntpfE73bImXXT7Vm2k/dO49s+1dDx1FaJ7eT/AMjCOWYqb2WveS/zONFrIfvEIPUmpltIuhct7gVua/4U1C3tPM8M3kesyIjGVCnlyAg/wIfvcdgc8GuHh1zVvD2m3Uev6Rdfabgf6I1zE0W3qGbJ644wMYzU/Xabtyu/9eZqsrrq6mkvnf8ALQ3DaswPlx4HqxqL7Kgb95Jn/drkrfWdYjle7kuHwydXAI9RgdKpQ61qptpVS4bYz72cnlT9ewpvF2eqEsvla6Z6NbNFGdtvaCRz6rvaluruWMlJkMbD+FhtI/CneC/E8Ok6NcahqUk63aAraMm1UlJU/fbhsBsHg8+lS+EvHupadqd/d6vA2sR3aAt5pVsMOj7mVjkDpz+dc7xm7UL289zqjgNIqdS197K6RkveTMuFbaBWfNfKmcHzH9v8a6C+n+Hl7MxS81fTpD87IxinBJ5I4249uv0FcxfT+H7K/ijhuLu6tmyWfy1jYDtgZOT69K3WMg9FozneAmn70k16/wBMh82a5fc77QOgHFXRqi2se24cMvYk/NVafXtNvbwWVlaW+mWYZgs7IzyYOPvnJJ6dgMZNWrr4e68Ua7gkhubXk+eN6KeM5yygdKz+uR76myy9z06LsNn1i3jhEoKHcMqA2SecdO340umh9dvBAI7i6XqUt0yyj1A6fiax5PDWoom5RDKMZwkyk4+maetrr+nW8sKW1zDHcKA4TP7xeoHHUd6JV3JWuioYOnB3SZdgs9RjaeS2UMYHCtA0i725xwmct746Vp2U8llrMaeJNOurey8zbMkKbJUHtvGOPQ1zEKagGUraTKV6ERtkYrXfUtfv9Ki0ueS9ltoZC6o4chWPX8PrUuTWkZGnsqe8o6r5/eeweG5/DsF1d33grxzLbOflks9QjER2Z4YtkAqD1OMgGuj1Xx1qUPge+1PTNQgOoRypE72WyaDB6yEgZB7fMOvrXgOnXFxpmp22oJZRGS3O4b1yHI/vKeCOxFdpB8TdUMdwLnS9PdpiAri2QBE7pgDDDpjPT3rzauF56im7T23tf0emp7GHx9OnSdNXp3vte2vVa6fIqtFrvjm8tp7u+sVuYgIRczsIWm5J+Y/xkdOOeQKxrvSrzT7j7PqEb2pYFl81Cu8ZxkA84OK1rnxvHMrQPo8W6TLuYWCZPQNsUBQQO+M1l3fiHTbm/WW80LyEb7yQsyHGOozmvRp1+SXLol2X/D/oePiMPCpFSTbl3d1f8P1KbSrHCVX0+8e1Ns8RmWYyOMxsFK46n/Jqtc65pctuIINMbgnfMZcMwzxweBj9afBrunKZYxph8jOY08zB98n1raWJjtZnNTwklq5Jf16G1o/iPXNDnim0zWLqARHiMSkx4PJBU8YP0qbWPF19rniGHU76SZvKZT5KzuEwDlgOeAeeB0zXNtq1m5MkVmkK4wSzlsH/ABrPuJpSgePzGweu3A/SsXUoc3Py+96G6jiVD2an7vqeyav8SoLmZ7q01nVNODW21IQxlfzOcjOQoXphuvtVb/heeu6XFbxWt22sbdpka9gWMHjlcryf97ivD/tk4uBOkxWQ8c/56V12k6LqV5Bb6ze6TDfwSybokjlSIyFTghgP4TjHQGvPqUcPFWlFW87f8P8AievTxWLk7qdn5X/K9vwOw1L9oHxxLKslrb6bYoQQFWEybueuWP4VrJ+0drj6M8f9g2CageBOrMUAx12euffFedeIPEGpXUP2HUCsFjEzG3soUUxW47qpA/rXKi+haLymt1XH8SjDA/1rCOFw0435F8jR5hi4yajUfz/p2Pq7wP8AGfQNb8NtL4luotO1S1wsyhTicdnQDJ+o7Gn3fxr8JQNILKwvbtgPlbYsav8AiTkflXzLod9ZabrVreFo7q1DAT29yOJV7jA5+mOhruLTSvCWq273T61PpbyS7UgePzFRe7FuPl7etRTy7BuTnJSt26fhqdEs1zBwUabjddXv970O6vfjhdSYi0fwzaRSs3ys7GTOfYAZNTw+Ddf8UFPFHxM1xtMso2/dWbsIcD+SA/Qsa5Nr86AhsvBdjZNdzgL/AGwkvmuF7hd/+rJ9a5zUrHWbi4M+va55sjDfunnMxP0xmuuGGpx/gWp+e8vl2/rQ5J4ms9cTerbp8ME/N2V/y82es2Pj3wN4W1TytMuYF09ywuVs7R3aRuzGRiCQD2A713a+I7SSSD7FZyzC4CujcLuVuhyxz9cCvn7w7qfhzw/cfbDYTapdBcAyFPLUnuFwR+JNdBqPxemuNkVrpttaBgVZ8l5W4wcMMbfwrz8TgVKp+7Tfdt7/AKnr4PMfZ0m60orsop6fPYv/ABI8fi31EWuiXKQ6rbhoJ5oCSsQzkorE4LZ4J2/jXmWq+MvEfiJBb6zrdzcRA8Rs2EP1UYFLLo+n37PeWspjV+TErlyG79ck1WHh+1f5ft728nUCVOP0r2sNSw9GKSWq6tanz2MqYrEzlJy919E9CqlvPeFIPO2xx8BpHwkYPX6fh1rfsL3w54cRbi2tf7b1ZcESXa4toTznEecuRxgtx7Vi/wBmSRym3kvoWjQnhWJJ+i1aXT40XdBaTSuw+VpQoA+ozXXUlTkrX07HBShUpu6Wvd629EV5rjVdf1SSeXzr++nbJwCzN9AOgHp0FdX4Et4jqAn8S3aQ6BZSB3gmxIJZj0VE55HUnsBzXHzNqsbFJLyO0RhgrAQCR6YXmrDXLCxiieEJGn3Sy/Mx+nvWdXmqQ9mtE+3T0NaDhSqe1k22u/X1/XuejTat4envXvtejN6bt/MhuZUAurXkqm3B2SKuAdpxgVT8S6HN4gu1lsdRs7/VxCJTM0hSS/TGFCKfl3LjBXOc1xUszy2yiNchFxtYZxz2H1qORrkCKSNmiZRjfvxjnv71zxw/I1KMrNf1/X+ep3VMYppxlC6evn/w/Zvb00PUtG1LUtIt4tVEs0UMkMaNEGMoidRscHGfl4DYI+Xn0qTVfGk2naU8mj3smi6jGdqwwIHgugWzkJ0Rueo4PtXnB1DWLe4XUGvZPnOWcAAk+o45+tJHqV5cs9wrpKyk4L4J55PX+lYfU4ylzys/621OiWZNQ9lFNP8ArVWa/Afq3iLxrrjNHfalqFz5gZTGchSDjIwMDHA49q7XwX4t1ex0+Sz8Y391e2O5RHAGf7RHjj74x8mOqk9q4aTUtQkO2XIVeuMjJqp9lubhmlSd1Q8gdT/OuqpQp1YezcUl5fp2OCjiatCp7SEnJ+f69z0C71OPTtU+0eFr2+u9Pk3JPp+phfJIJzleeMHkcAg85rnr6y1G/UQHVpJrUSGRLe4lZvLLHk55BPv3rngl1b3KRTKoQn5WZ+v0GasvBOiZgdlkB6k549qIU/Z7PXuKrifbpqcXbtdr7v8ALY2F8JQpMRPqCSRA4zECCw/4EOKjn8LWas4jurgYPy5VT+ZrEe5u1bdLK6sPlB3YyfTFWE81nVRPMz4ztYfzrVOq/tnPzYaKt7L8Td0fwroq6qg13Upzp5HL2oCup99wPH05qtrGm+HUknk0vUZEDNmO1kjz5a56F93JA9uay/NVnKXFnhOvmbv1pqCwkLbBG7A87TnFSudT5nJ/hYb9jOnyRgl5u9/0O08PP4H0hluFW+vr1ustzFEBF/1zXJAbOPmPpxUnjDV7HXbmMQj/AEdPnYTIu+STGCzOpy3HGM4rmmk0iBoo4xI42Al8bSG7jFDS6bJ2O087RkgVy+zXtPa6t+Z3+2tR+rrlUfL+tTNe1SxBnNwAxyFCpnr1zmqNzcQuQ7tl14HckfWtzdpUSF2XKjqGXIHvzVhbrSQvyxxnd8uFQDNdvt5LdXPMeEpt6TSX3nLmeH7PvS3aRuhyenpmqZuLhs4AjBPRRXWrc2aSYtkbywpUxKwVfrx396S18cxaAlxHpVjtMymOTaBtYHvk9T6GlLESitI6+o4YWi3aVRJehyqyzFtu4N74qzG0pIHlbs+lS/8ACTQlWWDRdPhZmzuaLzG/U4/SqLx6jdXf2lVuGlPAKIRj2GBxVLErrp6s5pYaF9Hf0RcWWDcFeVEPP3jjpU/+ieWzvqFsgUZxuJJ+mBWc2g3THM7CB25xKQCfw60n9hFSN07Ov+wo5/Ws3iYvaQ44NX1TIptVGWW3iZ8HG5uPxxUIvnuSEmvBacYO2MnPvxyKuppELyeViaVh1ClaP7K01VYyKVK8sGnA2j3pOunszohhYw1si/BLBDYKt7aiUyD5LgAjI9eDipf7Ns7q7W1glmhuCAcOu9Wz3GOce/NZuzT2gEULK6K3QT8fXvW1pGoW2nhAA3lnCkryQM56/wBOhrJ1ZxV0zWNGnzJTsl/XoXLrwUsVmt5a3Fzfxr/rRDEFZAOS2Cc7ffH1p9rpelyQRT2ljnbwwlkLlx1JHbPbpWzH4w0i3RX8m4EyjJZen6ngVzt14m8NXbGV91mxLIVtlzvHY89D71jGrWfx3/r0PSlTwi1otPy/4LOa1+K1OqGLTo5PML8xxtuQey980XHh/wAT2lubibS7pY1XeWUBgB74JxWroXiPw3a3LBElt2kYoZHbeQO3QdPetyPxtob3UltDfOpVgqnBVWPsf61rPESg0o6rzMKdCnUTdTR+R59Dqs0TjeiuO/8ACa3rS6s71lihJikY/KJf6EVoza/p11qslgbbzpVUyNIUXAx/tVbivLaG381GeMN8wWCMNvHqAAc0SxKav+pl9Ugnb9CudKuQjFNjyDgqDt4PuajeG8gCvPphkAyN27J+uB1qlL40s0uDDJ5skQJXLxAE+vFa1nrlmxaQSIkSLkkkHA7e+faodV2uylh6a+FmYdNFwxlt7pYj1ZTGTj8hTP8AhGppMuLxCmMk+W2fyrYsPFVhqC3CgSRSW/LNIN3y568f5FPvvEMMVo1zADKqjOV6fie1YPHJSUb2b2NFhKduYy7fwuNoYrczHPOFCD9ea0E8NCNQ32gxIeqBsn+VNtfEgvYFC7Un6KvJXPue1dN4Kd9f8XWWnxxytb+YZZJsZUKnLDPp0H41nUxySd5r9Tajg6dSSUY3uey+CdBTw54PstOUsZCDPKz9S78nP0GB+FdHig8nNFfE1JupJzluz9HpU40oKnDZaBS0lFQaBRRRQAd6KKKACij2o7UAFFHSigAqK5tob2ynsrhcw3MbQyA91YFT+hqWimB8WfCC8l+H3x9GhX58mGWeXRrnPAyWxGx/4EqfnTfjAkdr+0ZqcjjBF5Zv/wCOR/4V3Pxw+EnivVPHf/CWeDNLkvlvY0e5jgdVkinTjeASM7gFPHcGvHvHl14n1H4jvfeM7D+zdcYWv2iDbt+6qhXIycFgASPetN9Tjl7q5T3D9q5k8jwrMoyBNdJ+YQ1wPiPxIYv2VvBXh2M4lvr64kdQefKhlfH4bnH5V6N+1TFHH4X8LzMpyL2UZPvED/SuH+GvgDwF4k8H6d4i8a+OPscUU0sK6XJcxQqqK5OMsdwDEknAHU0LYqV+dpHtXwC0N9F+DWmSzIUm1SSS/YEc7WO1P/HVB/GvUx0rk9J8f/D++1Wz8N6D4k066u3Xy7e0tGLAKi5wCBgAKPXtXWVmzojZKyFopKWkUFJRRQAU+M4kVvQimUooA2u9ZXiHT11fw1qelMA32u2kiH1KnH64rSjbdEreopxHIPpQTKKlFxfU+DtrJlHGHXhh6EdadG22RT71v+N9NGkeP9d08LtSO7dkH+yx3D9Grna8zZn5JVp8kpU300NCuj8FX32LxZa7mwlwDA349P1ArmVO5QfWpopHhlSaM4eNg6n3ByK3i7O5x4es6FaNVfZaZ9J2zZArr/D0paCa3PRTuH49a4LSLxLyxt7tD8s0auPxGa7bw5MBPLATy67h+Fd7P2aElJJrZnFfFXSYhfWOrKsQaVDAzOByw5XntwT+VeZQiK9MsdpODNCdkiDIG703dDXuXxM0ybUvh7qP2YA3FqBdR5XP3fvf+O5r5Uk8R3lpN9mdmRb0MGmU4yemMkAj1yPWvPxOKzGEl9XnpHVK3337ry0PKxGFo87lKO52t3pUtnayXd08awoNzOfur9cVxx8ZaKjgw29xKS7ARsNhXA6jHBH1rn7Dxb9iivxd6xK00qERW5/e7WzgEZ46etVJtekjg+2SROqgeUss4LSSKc5XIxjp2qXjM1xM71W/d25LxT/O/wB5wrD4eHwr77M6628bW9zeYs7LzQ+STHmTjHQjoPworzG48XarcLLE7IsZ4RduQv8An1ordYfMXrzNf9vNjboJ2svuR9gi1jYgG+TOP4SR/MU8WcXOLsNjtu/+tXPmWYyqChycLxzg8dTVdIrkTkmXYob5juIwM8EHOOvrX4L9Xk95Hte0X8p03kxdPtI/76FK1vAOWnwfqKwYrqVg0XKujHJb7pHt70NNeecEMUbIMZIPQ/nz9Kn6tP8AmKU49jSc2BJ3Xe4D2BqvJHpGMs6n/gAzWSZbtLkD7MGi5DMpPy/h+tLcfbiGEFrCzBhnex+73Ix3xWqw0l9ozc0+hde30aQEbxg+gIqA6VochI81h7bv/rVAzSwSqZLcESng5x/nrUxhk3DGzeR8wHr35rRU5LaTMrxf2UQvpGiqRtvWXvxJimGxtFb9zqco+kwqvcJIJVWSzMkij7wOF/Om/ZnkX93CiEDneP0/Wt1Sl1kyf+3UW2sXwWTWZyPTd/gajaz1Lgx38zD/AHz/AI1CunO0hH2c5GCfnIH50R2ro4ASeFckcyZB/Wr5JJfF+BLit7fiWBp+r8ZvZuf+mh/xqRLDVlPEzN/vHNVXF1HEZA1wwHICsST7YoP9pjHlyThu4EnI+tC5+6DTzL32XUn/ANZHCw/24gf6UjabI3+s07T5M9c2y/8AxNZmdeLMP9IAB4Ikz/WnJJrmwM09yu7jG4DFaKdWO07BaPVMstoEMxJk8O6W5Ixn7MvP/jtV38HaVJ8z+GNNP+7Fj+Qqwup6zEMGa5+Xg7SGBqVNb1MIXFy74PdRWkcVi4/DVa9JP/MOWm94/gikngzRAwP/AAjFvu9ULKf50+LwVokcxlh0GW2dhgvFeSKSPT71Wv8AhJdTYsiB1de/ljBoXxFrb4+X5QcDcoGfcVssyzOC93ESX/b8v8yfY0OsV9yGx+C9N3rIiajC6tuUrqUoIPt81bUGisisG1LUXBP3ZrkTL+Tgis3+39RVW863ZVXuEBz+tVZPFV6H2iJhx12cGqWbZxssRL/wJv8AMtQoR2j+Buy6M0pYLqEsQbqEt7fn/wAh1l3HglrnaD4k1GIB9/yxQDJ/BBmqY8U3G8bi4I6gRnj6+lWF8X7eWYcHHSt455ncNq7/AA/yB08PLeJl6t8J7fWfJF94s1KUQgrGHjjIUE5I4ArM/wCFOQwq0UHiq5ET43I1uCGx0/irqG8aushU2zEeoTIqUeKkckmBSf8Arma648T59Bfxn90f8jOWEwkt4/n/AJnn83wMuXyYfFigH+9aH+jV0nhv4Ypo1jNb6oNJ1lmO6OSa1bcp7Akk8ewxW6PE0R6wjOOgQ04eJbfPMG4+2a0/1uzzrUv/ANux/wAiVg8IndR/M5HWPhfd3t2k2mW+iaYqkllg87D59d2QPw9a6nQYviNo0EtnLeaXPZ7QIoo2ZQgHRfmXp2NWx4lgJIFsf1pf+EifdmKzDR/8CyK0XGWc25ZOLXnFCWBw8Zc8Lp+TODl8CeK5pbyabStCkkuWkZf38ii33dNmMDj3FYn/AArDxvGGKPaFiOq3WD+ZFet/8JDu+/YOAOpBOP5U+PXoXTd9inX/AHsj+ldS48zmK1UH8v8AgnO8pwb7/wBfI8fk8B/EZQq+TFJt6bbtf8arv4I+JhRkaxlKN1VbpDn/AMer2pfENnjLROoHqcf0p/8Ab1jyAjfUk4q4+IGbLenB/J//ACQnk+Ff2n/XyPD7bwJ40jhuEvdIugso6IVbpyOh55+nWmW/h3xfbOki+GL4tDzEem0/ga91XXYVGUYIRz9401vENwQSojY9gT1/St4+IWYfaow/H/MX9jYfpN/h/keDTeGvFt7NLdXXhq9kmdtxYxYYk9TTX8OeJeS/hrUGIA4aNmLe2a96Gu3zf8sLf8WP+FSLqt0x5itf1P8AStl4i45f8uIfe/8AMh5JQf22fP8AD4X1kx5m8O6nGd33Ft3OB65NRy+G9WSJwmkasGHALWrZ+vAr6IGo3Df8srUfgalF3IRl/LT/AHc1a8RMZ/z4X3sP7Eo/z/gfOFj4Z1r7M8n2O/gkDEMkllIQ47H7vT61pSaHqVtZ+YtvqNwJGDun2STk/THv3r3v7ceguFH40fb5RkLdlh+FTLxDxTd3QX3/APAL/sela3M/6+Z80yaPqI3GLw5dyN1VTbSnb9eKjaz1xV40S/UKCFjWCUKM/hX02up+WDvYsf8AeobWgpBGef8AaNV/xEStLfDr/wACf+RP9jU0v4n4HzHaaf4guJRBDpVxAxO7fOjxqPrkV0j+HdW0+1+3Xdpp+phPmkiWOTeB7cDcfavd21ed/wDVrn6sagbU9a3HyoYwvbLNUPj+q5XVBL/t7/gFrJ6SVnK/y/4J4/ouraJOJ/tvhCaF0UmNY42iHHQ525P0rMvLuG5klxYzpzu/dwyNj2BOTXuB1PWs4cQD8Wo+36vuB/0cr67mrqpeIlSmv4Cf/b//AADGeTRmrOb/APAUeAC/ngMfl2N869CdhG0flVtJrq4I+zRSru42y27A57dsYr3ldT1BDlxCR7MRTjrN3jhYj/wI1tLxIqP/AJh//Jv/ALUzWRU0rc/4f8E8DD6gJzHdWtwccEFGA/AAU6d48K39m3IycZETAiveU1eUsfOWPA7Kxz+tSjWICPmjb8Go/wCIlz/6Bl/4F/8Aak/6vwf/AC9/D/gngdtFc7ZGWCS1kXlFaKRi/HHIUgVDDFrDHzbm3lRS3RYWZj+AFfQa6zCeAkv/AH3Ug1eHuk2PrS/4iVUv/u3/AJN/9qX/AKv02kvafh/wTwCKApPLN9mvnVBwgtmBYn3xSLNqiyNs0u+UkYH7twPbtX0Kmp2p5zL+Yp39owDkGT8xSfiVU64df+BP/wCRBcO01tU/D/gnzis/iV2/e6QQ7cB/s7ZGO/StCK6vGkMc+gXxYDIYRsuT+IxXvv8AaseMgv8Ai1RtqrHhV/N6l+Jk/wDoHX/gT/8AkR/6vw/5+/h/wTwg3kihSmj3pJGSoibjnpnHNQzarfxTBYtCu2j3DL+Q/T8ua93bVLsfdiTHvIf8KBqt/wD884wP+uhrOXiZVeioW+b/AMilkEFvU/D/AIJ4xBNDdQMZRf26qduw2LsW7kgA9Ki0Sy1C/vLkXUL2dsDiIfZ33H3IxznivcBqmoj7qxZ/3zTW1fVV5/cn23msf+Ij4jX9z+P/AADZZLRVrv8AB/5nh+oReItHuDENPe7idfleG3kPOe6kHBH9avWhvLjS2lfSby2uQx2q0D7fqTjNevHW9UOATCvsGP8AhUbaldyHa92qH2z/AIVD8RcS9Y0rP1/S1hf2JRvfmdu39M8GkfxDJfQSy6JcTRgENss3b6ZJGR+FVZ9G8e3xuFgtLgQSMcDy2Xj2BGRX0F9qucc36j65/wAKja+MfD6ko/4Cf8KP+IiYlrWgm/V/5ELJKUXpI+drnwn43uIIzc6bfSBCMARsSCOh6frWxqGjeMZdHt7b7FeSvGQWb7O4c5HTP8Q55z6V7gdSiHJ1EH6D/wCtT0v7c8i9Lf5+lJ+IuITTWGWnm/8AIf8AYsNvaP8AA+frfwF4puBEJgbfcMhZIpmK+x2oQK2I/As1hcK9xcardHegYW2mSY9zkkce+Pwr24anbAf6/OPej+2bRjgSD9azl4kY7pRS+b/yLWSYe1m/6+88i8WWbXbWTeHfBmq2jWjnzNlrI4uemMl+3H6ms/8AsrVrjXFuf+EZ1yxtGULJDDaM3bqobgc/WvajrsattXZgHGSTTTrcjfc8sj8azh4h4+K0pL5tjnk+Gm9Zfckjx+4sb/8AtC2SLwV4iurJXPnCZdjFfVdq4B69c1dvJdWm8RW39n/DS/h0hAFaKa1Lu3HUt144/rmvVBqV433WiA9gad9q1E8rKB9FqKnH+Nq/HTX/AIFL/McMow8E1H8keUa3/wAJLc2rWlp4H1MQscsrw4U+mAoGKx00nX0Rdvw+u5TkEl4pOnp/9evbi+pPw1yR7bcf0pn2e4cHfOx/E1NPjyvTjyqjH/wKX+Y55TCb5m390f8AI8QNj4tWOUQ+BJraQ/6uaK3Ksv5+1Y3/AAjPjxS5g0fU4/MJLYyM/rX0WlhuAJkBH1NP/s1AOQD+Nbx8RMRH4aEP/Jn+pLyaD15n+H+R80SeDfHU0m+TQb92/vMRnP50q+CfHeMLoV6AOPvLx+tfSxsUB6Y49aPskKgE4+u6r/4iNjXtRh+P+Yf2LS6tnzWvgbx43I0G7BJzkug/9mp6/Drx0+QdCmAPXMsf/wAVX0f5UC5+XJHYGo99u3Pkuf8AgJpf8RGzDpRh90v/AJIP7Fo/zP8AD/I8X0jwDqtlGrXOjTNdkZJZoyq+w+bFS6l4b8dq4tbDTQ8a/wDLaORBuH58V7J+4wG+yt/37pTLGv3bVvwSuR8eZhKXNKlF+t/8xrJ6P8z/AA/yPJ7L4fazqdlLH4hk8jKAoIdpcPn1zjGOT61Rb4QTDka1ghuhhBJHbo+M17J9s28CzcfWP/61H2xh862hz7xf/WqY8d5lDSnTil6P/M1/snD/AGm2eWWnwwisrcKdt9KzDcZbf+HvjD8cVDd/B5J5BJaXN5ZAk5Xy0kB+mXBH5mvW49Tn3EfZtuBnJj/+tT21S6A4h491NY/68ZopuajG/wA/yvY0/s3DWseR2nwZljYu2rXmc8MsUY47/wAdXYvg9dRyxyW+vajDjg7VjBA9sPXqKavPjmMfTFKdXuQceT+hqpcd5q9Go/d/wRrLMKtdTySb4KSPdNKdZvJS2SWaKPJ/8fqpe/B/xAtubXSb5ChIO+chGB/4CTXsn9rzj70Lfgp/wp/9pSMOYmX/AICf8KFx7m9rWj/4C/8AMl5XhfM8e0L4VeKdBmN0yabqUrKyNBPIfLIPqepNdPp/hvxPandL4Q8Mkr8oG9iAvfrXavfryWD/AIBqal9GScsePUNUy44zOfxRj/4C/wDMqOX0Y7N/h/kcLc6D4za+YWuiaDZWYbesET4DPjhjgda734W+Gtb0+81bXfEa2wuZ9ttarA+4JEDubsO+B+FBuVZlARmJ6YVq9IsoEstPhtz8pRfmzxz3/WvfybiDG5lKca8YxhFatKzv01v6m1DA04VFNNtonpaga7tU+9On4HNRNqdoOjsfotejWznLqOlSvBf9vL/M9lUKktost/jRVA6tbg8JIfypp1aHtC/5ivPlxTk8d8Qvx/yNPqlZ/ZNGis7+14e8Lj8RThqtqeokH4VcOJ8ontiI/O6/NCeErL7JfoqquoWbf8ttv+8CKmSeB/uTI3/Aq9KjmeCr6Ua0Zekk/wBTKVKpHeLJKKXHekxXomYfjRRRQAVjeKrrW7Hwhqt54as1vdZht2azt2XcJZOMDGRnv3FbNFAj52z+1TrXOdP0SNu3+jxEf+htWHdfs9fEbxNq8mr+K/GGmvfTBRJO3mTOQBgdFUcCvqTtRVcxn7NPdnzqv7MLX0iya/8AEXUL0jssBb8jI5/lW3ZfswfDyAhru+1i9YdczJHn/vlf617hiii7H7OPY4Lwz8H/AIf+EdZt9Z0TR5Y9Rt9wjnmupJCuQVPBOOhPau+pKKVy0ktgooopDD8aKKKAClpKKANS0bNsvtkVK33TVWxbKOvoc1boA+YPjppptPiJHegYS/tEfPqyEqf0215Zivof9oHTjNoWj6sqf8e1w0Dn2dcj9Vr55PWvPqq02fmub0/Z4ya76/f/AMEswHMePSpgarW5+Zh+NWhRHY+cqK0meu/Du/8AtHh1bdmy9rIY/wDgJ5H8z+Vej2Nwbe7huB/Awz9O9eJfDm98nXZ7JjhbmLco/wBpf/rE17JC2UAr0IO8UfqOR1/bYKDe60+7/gWPQpYo7iF43G5JFKn3BGK8Yj8L6Rp8s9vHZAHcVfcc5wSO9eu6TcG40uJifmX5D+FcX4msVstV3mSTFwPM+UcZ7183xBOtSw6q0pNWetnbf/gnsTSepwF14C8JXVo1pLpC+S7+YyoxTLdc5HNVbv4ceD763itrvS5Z4YeY43upCF/8erqWMX/PWX0+7/8AWpRsJ2rJMc99v/1q+G/tbGX/AI0vvZg6UXujiB8J/h6CQfDSdD1ml/8AiqK7byySADK3HU8UVos3xn/P6X/gTF7Cn/KvuOcupLRZQq3MhZyF2nBfI5PBH5iqP2qy1KQWVrNfGdiTiPahyEJ+Xfg+nPGfepJbPRrlrm+t5IxdRgC1Nydq5I9s4w2T1+tIZdV0nTby/l8M/bppjsmeNhN5oPVxjoR3ryoTgmkrP5pdTO767EM+txhYbnybjycbSzwMHDf7oXn3x0609r4R6lhbZpm2B/lUrgAY6sdpX3/U1blW5vbWFBpf2Oa7hzFG7riRxjG8Z+XjPPX9KrBrvTGM19exxlGEYhX95GiEdNuAQcj8fWlzQcdNX5Na/cC1LEks0jqws5miZvKWQIdhO3cvzZ4JAPXI47Uy1uJzNdk281vtkWINGd6tgc8g5+p24HTNQSJ4rTUoJozaXiRQSEvNEYC0bdt6r2HI9Kq29pqP2ppXZlOGiWSJycKQOgZgQQfYg9eK15aVrcy+/wD4YTTR0kyfavM23cbSxRCQiKQNt5PBx04569+lUJYpxGWMsbSSEbAjg56AZ478Vn2Wm2VobqTzLoiMZZ0I4cqdxTLKB24OfWsmKM2kC3JvrzTjMqmFbyFJ4pAeGBwcZ7jn0NbU8PdXT0Jcu508cd0ltEJkch/lkVnUvjoeDg+vTJoa3k89kcEBQH3BhtPrjrj6GsKyj1qe8M1jMdQSHb5zzBUTkZGwgknvnIxkYOTSalrV/Z3Dxz3Npb2EBCu8TM0oz0yCMH5uOBx+dNYeTlyqzYnJI2ftZZnMbhXQ+WfkJUNyRk+vGfoKWZnkjgV90Uw/eEJyrDHUn0rF0651DULFry6v7eCAMCkjIQsecYV2VuV5wD1HtUI13S4dVS21HVdN8+PdEsiyBlgJ/utnlsDnvjgcjFbPDN6LUFNG+kVwSrRozLJncSygcEfw5z7celTImy7KxOVDEECWQbT29Ny88cjrXNReItPiLiW2MBChY5BI7qWz1wxzg8/Tg1sWFzaTgxyWslqMKiuJFlSQscEblY4x6nn8a56lJwXM1oUpJ7M0YLh5SgATJyCDkHrggdqjlZUkkt41+aMkBpRtDD1yM57cVUl0eeNJhDM91b2bFVtVnH2lc4+XeOHHoG4xmiCW+N+6uiNBHErMsi/vYlb5QzFcr1HB57Vm6V9Y2aQ79xdOlvZbgRTlY44iI/MyrbzjngHrmrfkQ75HVR8hUbsdj0z+PoD/ADrMufOslkmttPnuEjcbscfLnAJBJHP0B/KrKi6uIPLs5WWbcCQQNvP3gDgjH4c44pKCbWq1Bakm6Vobhm0ybckPzBdpZuf4cH+eKmRbiWCOK4s5IiWVkYEFiPRlwCpPP+JrJgXXvKuLmxnhV+RNCEVxGM4Ukja20/z7VqQ3V6ktqRJFcyRr5U1oh2TEddwZyf1GPeqdPo+v/DC9SCxt7ueJzObiFt7osMm0kjBHBXqPSnJpEjZ87U3XJG2NgFwP546/So5ry/mnMos3cRXPli1jbZcFAeWK/cH581WvdVsp7u0+3RXemTylj9klVUMQ3fMzbuOeOe/tVKnJ6208he70JP7HnZRKl9MV2hc4CnGep9fy5qOTQriRkVbkNOf4ggA4PGfUYzTriHVmvI20nXbf7BsZMybhtH8IYgkMOvKjOetSLc6vdKwltXAgfy57neI1dCDl13A4UYHUbu9WlpfmQ7K9rFdNGvl3MVRwCQAGB3H2P+OKnl06+QwtAiea2Plc47dsdai0ia6ls2lYG1tdhULcy7WdjwpUAHPsx4Iq/NftaeQJZYbeLDJG8oG0uD93KnC8D1NZy+Kz3Fyq1xi29yqqk8cRwQpb7w68gn1rTNlavGI1gWORTk5PAB6Zrn59auhqsttd6RK7gObfyELrKBgblK8lQeueTUset6SlrJfOJLdYyplZkZVU5KlSDg57Y46e1S6Ulq0CaRsLY6c4BRIpGxnKsRj1+nNWFttOEqjEeRt4BBKZ9ec1jNqflRW1y8EU1tLvOIZCXY46MhPHruGarNe2F4AZNMmkLShItkRZiAD04AGDjgnOGHesuRl3tudN5Ue0g2x5/iHO7nv+FJJGGf5JVDdMMfzrCSKzvrNp/tc8aRuwRQuCrA/dIbnjoeQcYNWFJErxtBcSO6mZY13K7R45GW+XdnpkjPpWXKmVc0ZLMFAoxycYxnj/ACKHsYGiVmgUFuCQvf6Vjw6rfDy4ruyktWf53WfaCg/hB2nPzc8gcEc4zUj6rN5qpFD5rISVVXBfb1DkdcdQeODTUFcVzRbTLVVBCIHJ2nC9/fH0qBrKRRhdmNwPy888/pSRX08kbhbbbLG4LiRgu1SMg8H5h24P1p0Mr3QWKyaOWVXCFQ2N3BwVJGMjnr+lLl1sPRkZtCihXJDhiOmMj04pqJIZ1XzHAx3HftRc215a2xeFncDGA671B3fdypyO/ODUL3s6aikIRW3sFUFsZ4+b5jwcHtkHiqUbq6FYvxoTG67pCyjIAA5HoDUTX0co8oJIXAz8yFcfiaieW/njjEMEscwfYwUoBjOCXIPCjqcZ/HmqkGoAxJ50ttC6tnErMwB75K+vrjofrW0YoGX1nwoYIW+XJ4O4/hUR1JVCDCnIHLDHJ6dKgkvp4rf7VcwxRBJNqyQyblkHqAMMOOSMGi4ljJy5RJHDSLGkisxUHGRj6Z9ec4603BbtE3Ly3Fq20iBSG6ZUEHHvT5L5oFYRQ/cP8IA9vassWQB81TJHCy+YFlUDbu/unGc8Zx7jioUSVwZHuZUhbCgquGVx14yVKlecdqn2Meo7s2TM+5iY5lPDentjr71HJ56sfvkcAgsef1rMbUIEuobH7TJ5jEhZSpPmgDttztPTrx9M0yLF67GwuGMS7l27SNjjliSO2B2zng8Zq/ZILltydw3ecrDIVWkbBP59KIo4zAqqZAw6KZM0kMbtduEuX3xZ3qwGxTjOSQB0IxnnPFU7mO4gSW5W3lht1dVIkQgqCc7hg45II/EVHIu4npqaaRyGTbGc8dGfOKALnLLgBh0Pr9eaqRzCe0a9jQRwDLLG2TKcNgjaPfn2FSyhoh9uaZjak7QiEMZPQg9Tx19Kr2XUV7jja3S2+9cR85+cEcUmy6QKUUkA92B/Ws43hFqHF5Iu51xJNCVRBnGCflAPbBI/WrRknmEZsrxJwCeBjDkY4U/mOT1pqnd2aGvIs7roMCsHzcjG4A/nSfaJzy0LgY3N3I9hjg1myjVDMYPPRzIu8QSoUlGOSPlzxj0GfqDSRXt4jtb3Igt5ljLDfKVDrnjr0zg9ap0UkFzRjvIjhk3AbsHHb8KnOpJny/mDY6bevpXPGG4uP9Nju4YGwfLjE+Vcd+Djvjpx+tRiG8ZootQeKGdirbfNZGUbfvdMHPYZ9/Sl9Wi1e4XZ0/2zsHUE5yG4K017nYhkaYKDxj/PtXOiLU4Z4oZYYZzNGwiMN4rt7E7RgZ77h+JxUKi9KmW/xGikYWN9+xuw6DOT9Mk1P1VId2dQL5lYBXBYZGGOM09dTRwCj72J5VWGR/8AWrN063tEYPc6xb21tGHLnIl2k8jPPGOhx+VRTQ2slm1ykkcsZYAwuyxsQOd+R1XuO/4iuf2UZWaKTZrHX7KJ1Es3llyQoZhzj0pT4g0zK7rlQcEgbstxWXd6YljLbxnS2vreZixnifOzgEZBblffP9ai3wxzR+bpKQwFlT7QXjKRg/L8xyT36+9DpQ6fmh8z2N9Nc00xM+9yFYAtjjmrL6xpEjL5UjSKfl3DsfSuaEN+97FHb2Re33NG8rsmSAcBgAACPrzyKjt9Ou7iNnktY7SaQAJC53OvzZJbGV6enbHNL2UbXv8AkVdpHUSajYgqqRySMCQduTj64qN7qzJzJYMykDkgnj1rMhj1VJGtVsVmhQKwmgb5inqFPp0yDUzDUWtluIVifbhWR8qR6kYHXqcd/wAKy9l2HzXL23TnlRV08gv1O04/+t+NReXZM6+VYsdw3H29+T/SqMh12KZM2ts1sRuWR5cNk9ABjH69/aoUk1lr8u6x2yx5JJKuGYngLgkj8R3/ABqlFpf8ETRossR+ZLVc5/un5WPQEe/17VK9uFAKIhPBbMfbv3qoj6krMrD9z/DLzhjjO33P+TUX2i/QKWmgR5H2qjMMEcZIPHPtUO99hXLTW100xMUUbjthQOfXPFNKXsWW+zkEnlQM9PTn9acdRtB5kL3EEk6N5TqAeAeMYPJH0oXUVt1MNwyrIsgHy5DEYPQdiPeizS2HZED3V8jZht5G542jketCXeoxyZKyAA8rsJz6dM1Ot3ZSSBhqBHJJUDkdODx/nFWyGW8jUwl1Y8kNjIwMY45/Cmo3V7AlfZleO9nLoHWTaR2U/wA6sRXpALeWxUHD/N0NMRdQimRJjkEYLNHg5HsD+uKbGbhryWOa3Xy/vKu3LFfXj3yBUeyvqh69yX7TGrhmhk28HdvwG9hzTTqn7kSrAyAn+J/ftUluLdwIorY20gBATYcg9eM/X0qdbUeZuiWOSUEI0GwDPUnHGc9+MVoqKtsPXoVBqJ8ppAxKY657fWpBex+X5ry/IRn5jjFK32V4Z2FlGYsl8henPXIH6ZyM1VMcUsQuUIUszYIPTscjHt0pcqWwalqK7t5DshnRznJG/OO5pIL+xmjdoTEzgDOc4bvk+lQPLalZEZAzKP8AlkFOQeg3Dv8A402yeCV5BBY+SQwPmDkS47E9PwPqKdrq4I1fMtU2L5p/ecqyDcvIz16frTDJarI0ZlbcMtken+FUY5lltIzDp6xB3bcgfLZBOQeMHB7e1IDcNw1h5ahiCJH2yKFyRx0OQKi2tki7mlmMIACCCMjHJx60rO0SkqGdQeowePoaoH7K0os0SMoUZ87z+7GNwPsMAntUP221nKwO9wrunmlkQ8jO0nnjr270ezC5pTAtGJDCQBjIBGc+2fWoyheQx/Zm2j+Ittx/jUEUxQn7Osslu0gCu4O1hjDbR1HOKkmv4pEUJaTyI+AwwA0Z75GRkjPY0nHl3DRj18tW3SbEc4HJOD2zUwiT7+Vz2C8gmqU0hQJI1tJIEzuiChjJg5AzyOR7VZeYb3cQqAzcJ93A74Bz7enWk4Rcb8xSRaxHkgNE3AbGf50pIVgFKsMAA44/z71VjUKxaWJl3sRkRk4474zwRTRI8cLQrsvZIk3KflQvgZAJ/TPepUOa1g2LRuCThLYDBIb09qUTW4YZsyztxgYBP9KroUkEUpKwyDJ8vf8AKR7+nIHP+NQSXhObdJYVI+dgZFUbgRkZ5Oa0gr6php1NiyLfbVUQFFQbt38q2Gkkc5d2Y+pOazdNPmW3nf3iQPcA9avV8xmNaUqzpp6I+lwNJQpKXVi0c0YpjSxJ9+RF+pFeYj0bD6Krtf2S9bmP8Dmom1SxH/LbP0U1ahJ9C1Tm9osu0lUDrFkP4nP/AAGkGsWef+Wn/fNV7KfYr2NT+VmjRVD+17L1f/vmnDVrA/8ALUj6qaPZT7C9jU/lZfSaaM5jlZfoatR6pdpwzCQf7QrIGo2LDi4UfXIqVbm2b7txGf8AgQrtw+YY7CfwKso+jdvu2MZ4dS+OH4G/Hq0bYEsRX3U5q5Hc28uNkqk+h4Nc0CD91gfpSmvrcFx1mNCyrpVF5qz+9afgzz55fSl8Oh1VJXNxXlxCfklbHoeRV+HVwSBPF/wJP8K+5wPG+W4m0a96b89V96/VI8+pgKsdY6mrQahiuYJ/9XKGPp3/ACqavtqNelXgqlGSlF9U7r8DhlFxdpKwdKKKK2JCilooASilooASilpKACilxRQBZsmxOVz95a0KyrZtt1GfU4rVoA4b4sab/aPwt1lVXdJbotyv/AGBP6Zr5IPXNfcuo2aahp9zYyjMdzC8TA+jAj+tfD00D208ttIMSQu0bA+qnB/lXJXWqZ8RxFStVhU7q33f8ONibbKp/CrmaoHI6VdQ7lDeorGJ8bWjszT0S/8A7N1+xvc4WKZd3+6eD+hr6Gt3BHByK+aMZBHrXvnhS+a/8N2F05yzRBW+q8H+VdtB7o+u4Xr2dSg/X9H+h6J4cnw81uT94bx/WjxbETo63Q6wOMnH8J4/nisrTbj7PfwzZ4BwfoeK66/tVvtPuLN8bZUK/j2rnx+HWJw86Pdfj0/E+7R5aLwB1GC+/pswOB35NRtdMkyq8m7cONq8MT0rGvQ6PMLSymiuY1EewSc7t2Dtz8ucc8gdKks7u/SVY2tVZ8kSZ5k4HBUdGz7V+Lyi4arv5Db6Gul0jNnnIyD2P40VmQTXQMhmeNkUnCohQKp9T60Uua+ojzy2+G80t2ZIfElzeWW5JINsmFdiucb+SfoCMVb0nV9P0bUW0261CW33sUYRJtKuARkbfvH2bPSnHx5No1uvh6GxEM9vshlnvBskBwOSqcA++fTrWHdajNqurSXbWoluZ3OCEChnPHAHTP617Eo1Y1nBrnWyTS/pnizqRgk4HeaVrs06mC6hinCjYJ4tqOf+Ac/pU+oQRX8azB/JmU7obrzXJ80A4Bx2x2965PQbK6trtdWu7dFZP3awbs7mDcqy9eRnHb34qDW/EUlhNcSW/k6aocvsyWBPTaM5P4CvP+q1KEvdsm+ltflubKvzR1Ej1Px/Prd/A+lMryWwd4onJi8tflHlgMNxz2zk9TVzb4ksdPW3+0WWqSRlWmjJUzwv2wuOmOGwTn2rBu/H+uauII4oUtFWMIXUE7+OoX+HiotBuIhqgu1JF0qnc5k2b17gnpycH8q7qj9neXItE/n/AJL5sy543tzM6V/Dvi65gudQOjBbbUQqskc7SCTb2ZCf3Z44IAI7Gta+sNP1LTF0jUdCGnXdrhIxbuHMDMMgc9jnnd165qfT/E8zWm22iJXJWWWKTzBHxwQR0b60xpdGj1c6no2qmS4Mq/6PqVzwrYwAGHKknscj6VCxT5VKOj003123tt67XN+SNtNTlX0D4h2I+0WGp2TJgEpHNiPaeCGZh+vPqCKr2yaw9lNZ+JdRn0+aSUxrHbBJFUqc7mJJ3cdO3frXSaprOr2Vqg1XTbq1FwN63sFwr+Wc8DK5Vh6q3XtXAXfhXx/qWvf2jpj2wyftFvKJRGrE/wAQVuc8cjGK9XDz9pTUcRJJ26dPv0/M55pRdoXPWI5Le2srIqbO7UxFNkq7JLlgR02jbkA9CO/UVl6t4fBg+xWvhjQ51xJKbWchH8zsNwUruwfY+9cXpWmfEnwx4qtNUumSSEHEsUP79SoA8xSMbsn/AAxwK706ZZ+IpzrGi3uoWc7Psnti+3aQOYwrDCsBxn/IycIYZX503ol/S7/h5muslotTyO6svEOhRM0yaLp8NzG0TzZSV1APzKWBJDeverWjaZolzbtcw+Kzbvbnz1gaEwwzkD5gpPyk8cAjnpXU3+meDbPw/q7xaEZ2SHmEoZLguWA3I3LBwx78EE5rz258Q+HZILrTtI8E3L3zQMjKY3aSLjg9flx14Ar0qUoYqHNTT7f1+ZySi4tXPQbbxj4asBaW8trFYXca7sxAyNKG5GTnHPH/ANatS/8AFHhnWIoL+1aWz1GAbSLWRChX+INkbsjrjPp0rzPwv8NNf1x7Rb2eO1sUlFxcaabopMwzzwB8hI98juB1q5r/AMMNW8PC5msrhZ7OZ38iJd0k7Yy2CuBkjjJz055rCeGw7bhTmuZ77Py/q3zNVKajfod/f3N/c6CraXctqN8rKiRQqBJOhzubaDj5cDJPrXJad/bqanJdK12kiuYpI5mYsSeFHUrjPHFYHg6z1Cy1ma61W5Fm1nE4kjNyMZxgBlHOOece3rXa3fim0WyXTFtLu41CzOIylvsi8tsFWDuQuCCCCD6cVwTw1WknCEVPu/60Jc1LV6G7pVpe21zc2ep6ZdrcN+8eQgFAGH3SxyRyDwCR7CrF6thpMllqevWiQJGgKBpGIcE8YY5J57rjFedv4g+IHkLo50mTSXmnDwzTwsQijncDjHOeSuf1rT1mLxRcRiHUrK58T6ZbsWiW3KoSxX767TyM9V46ColgEqjcpW5rdU2vna2vy9DRT00PRbrUPD1naRXEVxc3Czy+Yb7LgxZ5CHb29+c1jX48MQJbwS6a16uoFbhJIFEoGf48EEAEdQa5Twtb3+l3NpO3263tXYeXby3HMEhHRwMds8DPPBxW54kstW1fUI5oBbxacWXc/wAxCSBSWL/XBwOma5pU0qvJza9LaKy+W/fU0dW8b21NxtIutMWztPD11YxRFWkj+0FkS15PyKq53gjnoMVn3Nx4nsp7fR5I7S6nnLSu8fLmPvnIAZCffPatxk0O50u3sVm1JmjjC+cJVSR1x93genGRz71DqGzQdEM2mvfHynTmFjcmNO4kRiCB/tKTjuK43NSqOMJRcrf3ld27vr5LRvqb2VrxObnvvCRlhe4spfD400bGjlhMqyTKdwXgEqMdA2R+VWLq60zUETVdE0tdRdm85kMaMkqHjK5Hyvu4GB1BBrobPV9Cn0O8Oq29tfNdAMv2ZcKwx7d/frVGeLQmjtol0NbYWsm5A74lgIHygvjcyjsM4FdU8RS5eeq9ey+7XRJbdHfXuZtO2ljBt/EazQi7jCQ3m4eRZSHJbLAFc7TtbA4wMDH1rSnXXjMza5oUF5pkkm9xayZeEH+F0fAfpjIwTzj0rOvJb68uprnRha2V9auFMahIkuA3IJIwcnn5ueeDU9h8QNUtNLSeDw1dPJyt1JOdp6/MERuSB6nqRxWkIK6kkuTu3r5WV/1Ii/5mZVno91qmpKNR0H7LYWpZUnljEbR85ykbYz2+U8e9bU/ixoLldKg0xYpreFU8u2jMrsCciXaRtI7nB4psXivwzrusA3l8sFwiEgXMrxBiTwrocBvqen0q5d6JDbfZLpWh05WlxNISUiGfusu3I+vTtXP7WUpclWPorNfi/wDgIpOy0ZiTieBBf3/jaSRZZQhNzaRgncemQQevpjHtWrqFr4ii1Uaja6klzIyMTDdEosSgZwpyxPTPzfXNZq+ILe68QrLrmobrraQ6wRJkEcDOAVwQMZxnvyKrXvjXW1gmtrXwlJJp0371GlDvGF24Y5xz78gewrrqYWtd6px7WSv27W+9marQZqRafr+p28+oPpOl+ZBDsM7XPnFWJG0nv/3z0z17VhW8HiibVJrHU9OtGt2ZQ1x9oCLt9OOXHqOOa3PC2g+TjUp71Yr4wkKsU3ml42HPAAwMcYzW1b6PpErJq48QrqRifzJ9PJVNiEEAtFwwIbBGfSueDhJ8tJK0Vq3dpa9LO/yNOWT1ZiweG5rOYI93p6TTFisXmsrhgfmYIScL/sjAHY1LcW+opnT47mwltrm4GTkwMc87TjcSflxkfWtu9h0f7ZFfx2TSPblRD9oCvkkEHd/EfUAGsPX9atLcW51O38mS2lE0M1lD+9jB4bAB4XGcgnFc0asaklGTUn2S69NW9vkO6W5Hqv8AbWh7LnQra3uB5bKsKzBTbk9QzH/WDvnOR04qDT7vUtQ02a0ljFpIZkaOOOUMJWwOUIOOnBz6cGu3tYNBkt4LtheNazhZAtzAuJMjJOOhz1xisaKTwfHqN9d2MNhY3Of30qygKRnjcPuqfXGM1TxNNQk5pOS0tG7v+NvwKlC3Ux7KeTTr6RbuK5ndw8a2+5pGOOGzHnHXgk8Ci8XXNUmhu7DTYdBufLJkDbHDMBxgjLKMZ9R7cVWtIRrd/Pf6Bai31FwyFLiTaWUHl2TJ4zjBHXNbtha61pt1Mt8WVLortNogkRMDBAZ/mUnn1BzxWnPGEntHye/yS0+fzJpq6MVo/FVkunW39k2s81vGGN6J4ykjN18sMBk4x15zgip7p9SmsGeHw5YuBKq3EeMCUAczBicdsHnPepNb0+61aKS007UVt9NVj5sEyfM0iE4O5Tx/u/TrWdaaRrC2H2TTNXsFlSTJWEtGhU9QSDuDZ9RW6lCS9xpy7apA01o9hmseI/DiCx+0O0OWAFuJN6hhwFDg58vGCOnJPpWg1y+pXXm21rs+zuoEqXC7GO3AAOcfLkjjPWs/WdF1vV7a7+12mkLeZRBcToMzhQAu0qAMdtxGev1rhdUk1OLUJtAJurNFkVkgEojUZHzcbtnP3gQa6aNCjiXKMJardX/Rf8ExnNxd7Hqq/wBnwXTvrOozW96sedm4KEVeTtyCCG9vQinpZyX9zLqOk3KixhaNo/KnMUkgI5CR52AnkEjHPNeWW+nprFxFc6jc3+mXGTvb7EyxErwpViSHzwSMeuK9E0zRNcuZTcXd/b2dsjAAeSzCUYHO4YVFPZeSO4qKtPllypp909Lad+pcLy3OfuPFUui6q8TR3l3ZrKTHmX99bt2TC8n0LE/QVrv4nlvHsY3sI4yH3XC3ExEocnIAX+NSMHkkelchc+G/FYvLp9V1m10yOETNbpFJvABO7YdoHB67jngdO1VPCmm2suryPrk+Jrdc2wibcA6/MEAUYJb04HXNdP1WlUi+WSvbo/8AhkZKcovlsekX1nY3TPeWWnWy3HknylLKqlzlc7lztI75HtgdayoG8TQ6k97e6dYXEdouJLSKSPzIsfMu0kZBP057VTuNa1Czha+0xLMNOpeYSYjZhkqSFwFBBBHr7Uw+LvEGiv8Abry7tdQig2SbbKRZlRWJAVnUD5uDnP1rjp0aji+WPNbRXdn/AMA3clui1ceI7y80ItqWiJbxSsAsczCTzwW5k2EAMBj8xWzFpvhi+t4tl5cKo3Em0+WIMFALHCjGAR6VU/4TzwDrH2T7VoaLNEo8zzMmWJi2RtPDdzkcjkY9agm0XxFq9xJP4f1KRYPPLWqbWMscY6BhJz+FFWEYNRjeDfTXd9NU7jd097lyXw/e2uovd3KoIIwEEqXLFpAPutjH7tm/Qk00eIpTG39u2cUJldmgaQK4by8YToWbqeW6k1Jo19qcEElvrkMerMJOI53EJII4VhyM+2e3SuhXS/D2o6ElnqdjDaRyqXkj88eWjg5HJI9OME81i5uElCsk23ayv9+v9eZSXNsznd2m3CjXP7HW0uYtywyxMq7XA3fNx8vB78cH61LBcWT6cZL+aG5tbpRN/rx+9+bjDAgkgjJAJ7Zq9PA9lelrTWGisXXy3geQSPJkcOpxv3ZxycjHcVCtsT4jgh1TSbPMyBbaF4xIsjj+Lc2WJx/Ce+KHWp6Pm06au/npb9Qs07EB0+/s2il0PV4YkLNJ9juFOy4UEENkfMFPQ/T2qGbU9Q0vQTqF8ltaSMds7WzeaIgWIXIHJGc8AntxWnqfhF7e2k1C1sYoNjGMTTzFY4VPKmNU5Ubj0PHJqgt8tgsVkl2sNzcor7nQyHG3l0JBDDOcZx6dqpvnipbx36rT8fxFZxdjMkHhnWDp0c0yRSuC6SSW5WRGJ+/5aEEA9DnJxzWk8FvFZwLqGqPpy42iV38/exPy7lBAAODgcnntVfUNN8Ya5p9qkOq6dcNBvZ94dWlQddwwccdQOtU77TtG8RiwiN4k32UC3uZLKQIiD+8qOMflg8d6alCylze7f1t99kgb01Ql9rei6bfXsltrpgUggRLGqcHqvclfTuKt2GtwXbLJpemjUJJ4X3QyYlhQKvO7I2qccgmqc2k2Ph6e88QxWFxrCCOSNbiX94LcFdqsx+90yCSMc1yupa3q91BF4f0S0jsbe9RJmjgYiOUHozLyFxg5xjjtXTHDwqRVSDfe70X4WIclHc73TrrxZexypFodrp08Kh1mknURLEOuSOR2HHrVv7TcR2Mz+LZbWxywQPaM5jC9m3Hgk89eMcVw7/Dvx+ml/bbm4KKF/dTeefJIB6Agcsew46GorWyTRbg2Wpi61TU7p1UxrNuyO2wAkN+P6YqauCSjGz3tZJO79Lvbz1uT7Rw+JHoa6hpGp3dtLpaudL8l0Z1OySRsnBUgYx37Z96qStd2s0tyNc8mVMOkLReWjr0O5h35GMj16Vn3XjXQxZXOnaXosgvo22E3Q27QOGBHZvpxXPzzTpcy23iDUjZM/wArWXkgFVIBB6nKnI5PX3rnWFrK8pRVu1tdfkndfcVKrHodp9m1i5smuNO1S2kCgSeZJK0i4ycoG6qM+mfpSPHr9ncRXCLbXyS7XlSEDcgHBUnI4yfvEVQ0ptFg8Otp9xJJFErNNEdNdYlGTyzbeSxAGfTjFNsfGNhpt8ttDbT3jQ5ykoLFlAz8xPJyO+KxnRqRnyRtLvo7pef/AAL/AKDU1pdlqOPX5NRnuHhhFruEu03B/wBZ0IOMgLjoRjOPetieHW7i0Wdp0tI4PuGPEgYZJII2j5cnoGzXPzT3/iaZNQSa3MkEikCOPzHUZyAVY8/iPWnxWPjb+2LqfzrRLJSrIWIzMO6qOTG3v0+tYTiua0pRXk/lb1/pjUyzK3iAySRSaTZ3C5YIwlDF+eDhlAQ4z3Iz6VmwazZ2moKl7pCRtBmFpHy0wLndjdkgZIBHJzzzW5Dp3jR7+VdU+xwW8kgCNaxGR1BBPQ9RgYzzg9cVZgtfBUkVzpSajvvdm2U2zmAGQHjhcgHJGRzW+tK6nC2217W+7+mVq9vxKtvqdxPq1ze2yMsJYwCNyI/n2ghMgYznOScHnvUOgR3V/dyyyXc0brKizBCrFZM4IU5BHqAfTis2+8KT3VoLPTLe1sLtJGWadp5QZxnhWA9Dnnvx0rTt/Dfi2CaMfb53zGI9wbz4Dx9wqwzkHoT17EdKd6Em7STlpfv/AF6DUZHQw2mppfSk6xbm3V33QXDASxp6tsGGI6nIzVMav5cM6z22ZYiSFjn3klRy23IwpGCM9eOOaqaj4b13Za6fq0El884EzalDcMi2rA/cI2n5/b39qpz+FdM/tlbyy1m9idU8tnW5K+Vx8pOzO7J4wv1IrSWHpKahUaU+2q327Ib5lqkX7PWYNVsZbiyul06dItx80h5H28s2I+Rxx9B0pmhXGvi8vJJpIYrby8pLGkiiYdCMkYB9s89qpz+G7rQNRtpLdgi3AWSOO7jXzXlzg7WU/MOeh556U60/t/xBeGz1PVE0ia2PmfZ/ILfJnByjjaw6DHJ5pVaTUuVR0W7u1Yal/Nubkl3emSS1tfsV9psbZmjEqxPkgE5VwQCOvUCpVms/tqxxCNp4wJY0DYxk9ckHIOP/AK+K5PVvBsulRXWsSW1zrUkTmSNQwZkQH5CUH3scg9cDGBWBZnxRr1neyLFFodubnzXu0heOUMBkYdz8y4PrgdOKqFKNWnz05+6uvTt3bJcnF2aO4m1iCwtftqWMchWTyUhbh5A3QnHLBSOp4qOfWbtbKNEtI7WSVxGss5ITOc/M2NoAPOAM5rPuJIvC2jD7Ppk/iB0AQTRiSecK3c4ACDP8SVgweGvGPiOeNVu/7Gs5ZeI72YvKMjJLYHJHbPJyKI0IP3W1ba+mvp1CUp9DtYJ8G3h1FbVJnLbHtBuUtu+Yjj5cnpjk89KW5n1KXSYZdE04TeU3lyLfs0LbRwCCOT278j1riI/D3irwtfTadYanLdwKpmcRDzEjkJwu9G+6dvI78g1X03WPE76nHHrF7baMiTCARXryI7EH5tny5LHPTGBx+Lnlr9q+RptWbTdu/wDw+4lN7SudjcS38cEMWraJpVtqNyDFCvnKWuhzkAYJ4B6GpYtUubTS4nvLay0+FV3Msb+bGvOPTGfbn9KwG1XwSIZriaGObyg0ccglZGZc84wQQSeemeR15qlqfjgaVqdrY22ny/ZnjURPDECsgPICqoAb3PX3rL2M6z9nSg7/AOW9nf8ARlc/Kdcmq3OoaLNcQXFvIyfu1RJWVZFckZBZcL1PA6GrFne66bGSObTorS/BEkStMP3irwWAAwBjqabp3iawvNOFm8Atp5SHeCWPYIyeArDng9fzNY96dBsjDpOvajBfbpDJFPFcl/PycYdQflC9P8aydFxcvddr9tV6fduVKSSvc2X1jVjqEcsbWht1RTLIZwrxN/CQOeR22nB6U+D+3Le5Emq2lvCiyFWYTsrdDhs7SB1zg9axku9R08NLaaVDceUhQXjSZQ4/hZG6E44IPJ+tUtK1TxreRytdzWFnJKC8EALeZEOQrgjtngg5B56U5Um6anzRsu/+Xfv2GpXeh1cd3eTOTBdvPp7boleOLKb/AOIbtpBbGOvTnFZ6teRzXEMU0N9CCojuLIgtjr8w52EHrzisyx/4Ti/eJBqdtaSxllmQ/N+8B5Dcd88Y6ZBrZX4dW9i9lfz3Ut/PIDNcxC4ZCj4yOmDtB655Y9CKtYSMIP2s4pJLW93vbpbf5le9LZFi1u7CeO2v7O0uEnuY/LjnuY8+aQOhZe2f4eB+dLBbxSiSa70eGK/3FpmVCm/PU5Pc9fxrz+81HW9N1WWPTry8iwyxAuzb9uc5UBeQR0AOOtdHpeleIW1WLWtW1aa4tnDbQIliDHoAyg54znkUYik6UHVeitfr8umhphoOvWjSitW7f8E75dUhtoEgtLc7I12jccVA+rXr9GCD/ZFUAcjinYr4n2ULttXZ+pQw1KmklEke4uJf9ZM7fVqj/ClFGK0SS2NkkthMUYp2MCq8t7Zwf666hj/3nAqoxcnaKuROpCCvN2XmTYpcVkSeJtBiOG1OHP8Asnd/KqsnjPQI+ly8n+5Gxruhl+Mn8NKX3M8ypneW0vjxEF/28v8AM6HAoxXMHx1oSjpct9Iv/r00+PtF7QXR/wCAD/Gt1k+Pf/LlnG+J8oj/AMxMfvOpxRiuVHj3Rc8wXQ/4AP8AGpF8c6E3VrhPrF/hSeT49b0ZfcOPEuUS2xMfvt+Z0ysynKsV+hxVqPULyP7s7MPRua5iPxd4fk4+3hD/ALaMP6Vdi1rSJ/8AValbt7eYBXJVwGIj/EpSXqn/AJHdTzPL8RpCtCX/AG9F/qdJHrcg4mhDe6nFXodTs5MZkMZ9HGK5tXjkGUdWHqpzS815ssNB9LHS8NSmrx/A7BWDDcjBh2IOatw6jdQ8CTevo/NcMkkkTbo3ZD/snFaEWr3MYAk2yj/aGD+dVh5YrBz9phaji/J2/wCHOOrgOZW0Z3MWrwsP30bRn1XkVeinhnXMUiv7A8/lXBx61bt/rEeM+o5FXYby2lIMVwhPpnBr7DB8b5lh/dxUFUXfZ/etPwPGrZSlqk1+K/r5nZ4ornotQuohgSll9G5q3HrH/PWH8UP+NfaYPjfLMRZVW6b81dfer/ikeXPAVY7amrS1UTUbOTjzdh9HGKtIyuMowYeoOa+tw2PwuLV8PVjL0aZxzpzh8SsLRRRXaZhRRRQAA4dT6HNbNYxGa14zuiQ+oFACk4Ga+PfiPpn9k/ErXbYLtR7g3Cf7sgDfzJr7DIyMV83fHzT/ALP4y07UFXC3dnsY+rIx/owrCurxPneIKXNhVP8Ala/HQ8gqxAcpj0qv3qSBsS49a44vU/PaivEtivVvhrqBk0i5sGbJtpd6j/Zb/wCuDXlIrqPA169p4piiDYW6UxH3PUfyrqpO0jsybEewxsH0en3/APBse5o2RXd6dcfadMglz8xXB+o4rz6F8oDXWeGroNHNat1U71/rXW0fqyPOfGuiz2fjCWeLURb2Vx/pRiXbGd2CD82QTzzjv0rkzpfiL7Uv2TUdtsWy8qjZJHGBgqoGcknuMcda9N+KXg+28SaXZXbp/pFnIVEmQu1G6kk9gQDXA3c/iiwsDHZy2+pSNEI0DSBF44DDPBJHOD3r8tzmisPjJcrV3qk0lo+t/J33Go33Whm3eha7bwyr9unuVkky0JIWZRj+7nBP5HniiqbXvjWad7O/kt7CBmBFwJiCxHVQBz+Bx060V5CjNK0pR+++nyViOWL1scu+oSazOuneLdFs9I1lkVrWWzVnkmIGNko3njA+8ckVNquq6MHdre+SwmtQys8JBkLBfugZ4543duSa4zxg91Y+MLXU9IurH+0ZAJIoZh9lMqA4KOrHax7ZDAn61m3fw9+Ilvfs9j4dltlulWaM+YskUSvg4Lk4GM4yf/r19XTw1PFcmLTa7HkShJra5paj4h8W2epRpJJc6dfwful82fI56qcDA4zxmtLQ9Utblk0/VNGN8bkkRXNvy4Dcchm5APTHI9ao2Hwn8TXFtFPqev2cMwJla2VmmJYHj5x8vPr2yar3Xgr4hWV1a28qW9vayOALyGZdkYB3Ek8MAME49q05sFVvh4STlrs9TBwrQd7aHqmn6ppOk+HYLjUrWSzaVcxwyJ+9jCDn5euRivPbj7T4s8Xpa2mnzadFeXEcEc06BEzznBXg5HP4V2Gr3Gtjw032rVkv0523jxRl3IXBXcq5GfTPfrXF6JqPiTSPE8E2hyCS5mjJEE4DpKp65U9PY8Hjg1yYTLfZJzn8T8/6/EmpXhzqHQ96bRrXQ9Ch+zJHcLbyoDJH+7flCGc446evY+1cDbad4FvvGg1WDyLi4gHmPbhsxykniXae/B9u+Kqr4/8AEd74Y8SRan4Ris7a0sWU3MKSJllB2DJJDc54Ocg1wXgvWdGhu3n8VX9zvkYSxpYyrvU4x85xgcYwF5GOcUq2Cl7FwpycXbf+v0Opz1Vtj6phvdM1DSPJsBFEPLZXtygcIB1+Q9Rz096808S32kaMii8t5Uc7H+0WYZQFGQnDZCjOflDD6VyXgPV7C+1y/wBOk8Vi1RxLFaXd022UljwwzwrY9T3Ir2abTNc0bRgXvv7ZstpWWOa3VxImM7sA4Y9scZ4ri+o4mpJOq24Wtpp87fmdXOqkbpHiOvHxlrGp21vpkyGxliRlk83aI3/iUjqT9O1d3ZWOrf2DNJ4gv2LRxrJ9o05FhfcgwruWzvIHoAeOaZ4f8a+Gpr64/tLRrjSpkm+WMWxdFBPPQfKe/HHpVzWfFemXenNJ4ZWyu7fznhuk5IZTznHBUdiCM59q4sXTrUlDmUVBO22rXRu5nT9mk5X1ORm+Hk+nTS3Fszaoyt88F1cNCJeCQwZTz0PB9RzXnmueN57oJJpH2u0iddsqMf3Zbplc5OSODz/OvRbS18QeKZYfs9zL9ksh5Xmb8hQOo4AJOMYHPAHNdlpHhazVVjn0+C6iGQ630CvlCOwH3DmuzA88tZq/nfp6bdjnnFvSGiPG/Clz4mQWurX8ksOnTEiFXTicdCd38I69OTivUovEOg3FnM8USNfXAe6ZJFModwQhLbumc9u3pVzXbfwVoujR6U8TQx20UlzFbySknHOdpzkYJyM5AweDXmK+KrCGBGtmE0UbAbEkB3ZP5kk59fwrrxdOnFXo07z6W2+ZjG9OVr3OzuPDs1zexX3hS6XTb6aMR3S+WJEnC9NobcwwB3POB6VjeIvCesXerDU7nxRHGrFViSe33bkVQOq4568Y4yK0rbVtE1BYUsb6UXCoHuIpHZDE+B0fADAHI4Naeqa/ALPF1qSG0jIkV3Kj5tuCcnn171xLH1cL7uJlefRKzXlft+Zs4xmZWseJrzSdGt7KZHuxu3RyZKEYHII/75x+NcWPifPZXax3ZmlhUF0ihcKU9N2eox2NW/EuvjV/C2zSRdXUst2IebU+VIpU/LG/dlwCRwcV33hD4Y+EtU8P/briwOovtUfY7sjzIZABvXKkd8kEDGCOtdmHo0a0faYmHvO+g3RqSlozgfEfja/TQbaKGKC1eYrKNhjm3LzgbhnHuuetciPHPidvtSx362mn7TG9vIoYFCc7Txlv6dq9017wh4GsJ7C2vtOhuIFc26aTGpkS2Z8sJeMOB8pBBJHOeK8q13TfgreXp1HS/Gup6ZNE4KwJbtdIxH91WGcexNd1GlTjFQgtFto3ZfmROlNNtyOXs/EWuCyna2ne+tlcR72Y/um7DrkA5GK0tO+IviGymZrm8dUtmZZNxG3PQDOD0OfXNeg+EPhd4C1zTo9Z8N+J7n+0EkaVrSdFj845yqvET8i5HuMGr/iX9ni6vre91nT3hsrhUR4bRfutIzfvFcjPAJ4ZeoxxWawtCvJ+4mvQtUKqV46mf4G+JNlq9i+mR+GLcPb4U31lEIkhXBxvDdAcHkc+1dTd6HNe3kA/tqGIyqskM6qXhbPQZ4zzgckdR614frSjwai6RHr9rLf3cKPdfY3GIh837vcOGHuK6n4caZqHiiS4tdI8T7ktxEk1ioZW2u4BcNnaMHJGM9OlctfL1Wq+1S1Wi/rzuVTqTdqbWp2Wq61qnhSxuk1S1sdUvLRGniFqgHmoMZXaRuU9889D6GuLtfGV14miI1Gz1LS4ZpD5c8iqizN1KBmHB5yD0xWlPYeOrv49X3hzSNRe5sbKSMXl1NbiKKJdoYoMAjJ3YGOTkk969g/4VnoNxFex3UX2179xI6TOx/eYxlG42kj0GOBxTlhIRtCUI3Xne3y2NVCUro+YNdsLPQbqK5Z5b+2ndjmKQDzl7EPzg9iOT+dey+G/F/ii28GRXjeG317w2LVWDxBfOtcJhs/30HIyQcc89qoTfC74dReczX2r3ESyuEiilyY2HDAKUAPTqeuK8v1PxF4l0qTU/CWgO0ujpO8JuJZNnmpvHAXO1emCAOea78PJTaSmnJXvsvvWpzWdJt7I7Cy1LSNP0SO9EcEizsXkVJNzxkdMlsEcds4+tWE+NsFnb/YLLTXd+X3524XHU8EMo6kAdutcNqvgnWJPBMniRdDeOxilJlnivPtDQJtypaMKCE5+9zj2rhNKZdW1S00qOSQzTusakDKqpODjvxknHescPltJzlWk23qNKaSt+B6lda5bahqKWVxrifZ0jEkl+ke2LG3pGiAMAScEEEjpW/dv4T0c6VeWuqwXExhE1w0EZuIX5xhpAytGPqCc4Hse7034SeHhpb2q+ErUFFCQ3E0rNNL6lwTty3fGMZ46Vl6z8PfDdpoOpXEFkvh25tIGlBtCdjsg3BZIySGyVHoc4Oa4qmIw8JwpunJc22ll8/6ZoqMlq3cwdb+IKxOh0cGTbBuee5g8qSPr1QY7dGAGQelcdcR6hrGm3OrTSLJmVVa6efHlkH5kdOqgggg4xxgda4u1OpeJPEDxQXMMN1ckR4zgSH0J5wMDJP8AjXr2hfCuX7DLFe+JbtpLlFDQwYEaqvO05yWAPIPGMdKeJeEwDdWu0py6Ja/1/W5zqMq2xh6N421LTY9O0SxsptVupHaOESsPLA7jax+VcZ54xg812Do2tQSW0Phm2nuI4FDNCyK0L5yuyPOScZGW5NReGvgw2q6fezalDNGHfy4JLW7Dl0U/eO5cjJ7Zx2rM8W/Dq00e5ttUu2u5yhjt4/Nk2lNo4yVwTwOPyrKq6FK0UpJS1uk7NvXfY1UJxheTNrwzaeMtJ1y2vLiwS202ZjHtvG2eZ/sgj5lY4OOnQ9a9Tm1ObT2YQ2zhwp8pgwZgT7fxEc4AAzjOK4SO5vNQ0WZ9X1G4g2SgwzNPwxBw2c9OD1rkE8XHUDc2888r+UGiV2ctsHYpnnp/OuWdKNSMa9OyivJOV77p9DSNZU1ypM6l/E+l+JftkkVu0MgJiaaQLbl2I+8QByR7811Fjok1/wCFRDd3Cx20LZje2GZiAMZBAwc5x3znnpXn1n8T7HRbYRwW6mfecXE0IMwOMH/JH41seC/GMSW/2aK4uPs8jGURzdOT19h9PSsasfYwdbmnrvvfp16/caU5xk/eZvah4M0kNBPeT32n21phUnAYefhRwwO5cerYzUE+jWbaiz2cUdnekxu0rkXDuvUMDxww6EAfmK9Aez1nUFlm0ySO4sHiAUXDbRcBhz04GDx07HNeYx6V4lvLy6uba2R7m2Do9tOvllY48DYJM7SQMYHfrVTwOKqKMYK3nvfy811t+ZpUtB7XuRtrniiw1G6tzoMd9BG4eHUIY96omRndnJT0PGPQ0+71zVbmESaJGt1qAZhMgy24noSM4BHrwcHnNefSaug1OQ6hfak0EbAxxeasRR++OzBe2RV238TWOnRslrozWdneyHztRmiLyTY6lc/LkZ7DjOcZrV4RpKDvaK6bP79jl9sm3qW7aXQr5re68WajMl9JG1vhbg+UhJ4yCPvD2+UHscUmsW9pLYvoPhSOW71CB9sKJLGJjtJ3KWyC5YnIAXIxxXKaBplhqniZorzT5r3TN2LZlnczq3YgdDz2Ir31Le/0K0hurXSPsEiqykqqqyjHzMxPJJ9evWtvaujOMVGU11UUrLtf/gsdO8lqeHQeB/GF9Iza6bbSnSNm8qUEzSYOeSOCwz/ERwOtdBDo8OkwJp9pqEc6xF1FwsARpN3Ut8x3egz/AA/nVpNau/EWqSWFle2cEtvmK2N/IYWlT+5ubjIOcZwcEdag13wr4m0Pw3cXunpZ6lFE4aW5trkSmzHdTEQCR7849K7JUsViI88Gl6Lp+OqIunrBX8yW/wBW8JNYQmHT7WxmhCpOstqxMhQ8rFJu3HcBgZBIz1rUXx1aizWHw1YnT1WRZWnuVZp0jB/1bAcDI4OeMetfOP8AalzPqDXE7yXsz5B3ktgH0Hb8OlbsWr2QuvtN89zYSM6qkUcZEZUDqXJ4P5+pro+oyh7yd5d7J/cT7Wb2PoWfUm1C783xR/Y9vG5/dCWRv3mPunPHIHfPf0rEOqpLr19p2laSbu8JDJBdXHltat1IQ7huToQCM89SK4yzuJdbe0ihguNatpXcW1qnGXwAV3g/Jxg88EA4rU1nwl4808x3dxJZWNtENyFdsYhcdED7RuOOQc4IB7ivPoUVytYiXNJ9/wBL3aN7yeyNnV9Y1610ieA+HIbVvOZPtcrgvbzgAtnkAfL2bgggis6w8T63b6rpLG18wzZVJ7dVnV8/Kx5PCjPLdsda43VLWTW/Ck+o6vrskl7a5hhAcMZiOVDA8kc8N2H5V1GgaDcaN4YsdflRLXzbZpUnEpmEAHDDPQFueBn35qalHCRhzRi3y6ef39Vr5gua90et3lxrGhSXMF5bJK8kagSCUkqwOd23Htwc1jxa5a3MF5PJYwJuXcZLteJD0OMcjA71hT+O/E/i3TbK3s9OCOF8ldUYbdwBGRzw3vgHqa8svPGl/OZdP1Ro/Jhdt7AYMmMjbgYJ5rgeWylV5MO7RVvJpXb1S36mkq9teh7xo8Gm2DK0WqTfZHwZknG8l++yXghcf3hmsTxBqGgy6fdWenzQWyzXKeUsqFlICkuGZCOScY74zzXK+G/7T8VaRHFo4uL1rXarJF83k4UBTIvqQOG56V058CeKLLRLzUtQR5khO9t7ooVMckAdSPer+q1qcpyjFu/91L5qy/Ujmc1aK0NjSILKLRRaWMsV1p1wrxyQTyZZUPLq2eSCScdsH61heP7/AMIaB4eSXT4rzTtYmhaNJLNAomwQAh4K8YHOQwrP1S1e4ijtftTTRPGq77XMLyHORyOQedpx1/GtnT7rw74W8NxrrukxGMM5FvI3mjkD+Fs/vAc5Oe9c1Kq6dVurK93a2+q7b7vstupabkmopLzOY0PVviCnh54F1PUdF0iVSsTaquH8zHVAQCRng4HGc81leI/CzaisWp3Hiyzh12OGLzYIEdS6KpDFCQAWPsQOCK1/EXxY06dLaCPlYhhBMu/YB0Cjt9a7qx8P6d4t8PWN7Nay3cFxbCSG5t18p03jkD3BHTkZFd/13F037X2LUdlpdr/gGfLGb5U7nAWvg/xDrkqSanqFnbwxlnGoStl5k6qSidABx26/So4fhd4q1fWHt5/sNrZKqj7cbjeki/w7CnJ+mBiu18IfDWXw9qTXOqeIrt7C18xbeBgI8LIRkyHoeQOOmR26VtTQnTb2f7Dcy27MVmeMONiHswUjgnHNNZrh6DVSrLmv1Sa+9P8Aq+hf1fS7VvmecX/w/wBQ8ONcQ3PjS20++nZTb74l8iZOigZORk8HGSO471JrWg+MhbTLov2XxL9jRV1G2slVjBIOdvlH5mXoc4x1GKxvGGnSeIvGd3q+reIPs1g6q0caxltmFCs3OFUEg896Z4YbRfDGtRahZ+I5obtGBilKsglJ6+xA784+te5GFFP2s1d77aq+vbocUppS5enqZ2lWvii5kXUbGzuLG7Lsf3z7ASevB5A9sflivUbDWtV0bQ473XIIXtkAmLvuWUx52vwAd3J7gZrJtvH8MXiBota8OnW5Jyz7LZzI5778DjP1/Ss/xP8AEHSBeudD1m90qUuCLE2wBjOMFGJwT+JPWvKq4WvXcatSMbX7Nu3rZr/I1hKEVzRZ0HhrXNe1HWNQbT4U07w++Y7Vy+yRiehQf3/p04xWhLPpI1z7fc2Ud9ewKVNzIMMT06jBJ9zyO1eM6v8AE3WZJFt1tYUiWUTBFYhCy9H2g4JyM+ma07Xxot5JcrBot3ea5etsVV+QyEgFZwighm4IK985zmoq4Cs1F0JOLXS+nrbX8i41b6XO01PxnHaa9b6dbWzy3Es4McaOGZNwwsf+1k4wTyBWunibx3FcEafYLbzIcmNpgCuOu4DOe/GK8IttD13UdUuIltLi51Jmb5FVmkDZ5yOuf5V6V4Q8eeLdKmn0LULePVHb5FN9OI2g2g7grsRk4J4znjit1lMW41Ipua6rTXy2/MinWcpWk2jv7PxnBdSNcXV0i3MrNHMWDHgjnI6FCcjHUdsU+81rwc8kLafp1ja3URDqbgM7Pt6+UCwCn03fWvGfFPijS9E1F7XRtZg1gSxo/mRxkcMM4btkce/PauTsfEk99qUUWqMJoiNjShhGUX1zyMjP41jh8rxcU1KWnnZu/k3r9zVzSdZp2a1Poy48b28FvcwjUY0k3eZbtK24xqOuPVves7S/Eusa7qdlFGywpIwZpbhwEMR/5aELkhumF4Bz2qPw54A0KbwvNq2j6gfENw8ckYjWMuEwDlQcAqzD+8vI4UiuF8NazLF44+ywQtZQGKSMWUMRxI6jjcCM7upz7YqKuWVsPSdnz6Xt0flp3H7Wacebqe13ltfafOojmuLyORxC7RIp2kE5JBOQMEcDqKhudK1C5vLrSvErxR2kSEi5gBK3ELDhQh65z1yMYOaj1OfXtB0aDVmsxcM+VYyDJUqoZFJHQHJGDyMYzzVE+NdR17T4bXUIrW1uU3KJC4VQeqhuyZPGT0yK890IUpSjUpcstHG/4p6p/wDDas7HJbMl0zwzoGnaeg8M37affqpi43SoRkZbLNlWx6flVHWb7WtKmtp9VtkmtTOYYb62kUAuRkKVH3WIHXOOMcGuHudfmu/tFrp2ltFqMEv7t4JSFcn7yMS2Dxk5H9aoafaeKtQkefV7i/tbON90spKleegRO5wO3QAVrCn7rlipJ+787931+7c5XPpTX+R3+uS3N+iXc+pvdCQM81qbZncHGMDacMcDtWLY+CfDWvXcE9tqurTPboZfsktyxIGeSN43Jz1Hriu5+GcmieKo5riS1u/9GQExOmAr/dPzA5J4zzjrWh4n0HV9NnmvtOna8gkIG0gGeHg8DI+Ye3X+dbfVsXGg69Fu7e0X97d03f8AE3UU/eZ41490Xw54MvgzwyXUuqRGY+YwAXaQCu5epJwd2AOlXfBWiXXiC8sr2GfUtG0jzUnjt79VP2og87NuAVPTJAznoa7eXSTqFtbDW9Ps9VntX8+Cd1ymGQdj1BBBIIxke1O1a81H/QvJsjdYjZZvmVdvICKme2OuemBWTzODi6f2/tXem9rK/Wxm4Jyv0MHX9D8Lad4hS2uPErHVbpipiiGxChGVXg56gAHgZ6Vmf8IH4ctLSC71DWZ4PPkzGbqRnlCqfmVQOuc4y3T36VqXOn6Lqd9HrGpaBDDqZ2RCWabc6BDlcbTg/XjjrXNvDrWo6/NHe+GWNuMxwywT7pBz8rZyVP0wKuniYVJXpT5Eo2s2rX8t79fzFOKXS/3nXrp+oW1s6eEtUWWG8lWG2hmfzmtHBGSVwSeOOcjkcVbf4bSDUrm98Q+JJrjUfKjSOe0UQbMddy85AHAAxXD61pGuaDcpY2DK8KIrXVzDcgNA5PyrgZIPfI496kvPibr2kmC4lRL64U5mctu+hyeWz69OKueGxNL3aElzyeukdra38/uHzQWkker29pJZwQ2U0jzwQsQzyfu5yjHJb075watG68O21xDd3MNteTQZ8qRi2JFxwpyefXJHvivOrX4kQajq6XWslNPhM4PmM2fP4xz6LkEHj0rRuPC89zc22oaDrCKojG+PAkSRGyR+7IADDPrggZ4rgp0atGpJcyg+a6cktUtld3XqlsdKkmtFc0Nb1TxLG8ctpJYS24IaNktTK6NzyQOn1HarcVxPeKLqdpCZACI5EC+Xxggd8EgnnnmvM11XxBpcsH9o6KVG9/mnQrGhyVGwLx09eueOBzPeeM9aul2xvHbLjH7pefzNbyynEYmgqdFrV6u+mn3io57g8pqurilJytokr/nZHo8k8UCb55UiUdSzYFY934u0O1yBdG4YdoV3fr0rzGe4nuZPMuJnmY93Ymo66sNwpRjrXm36aL9f0PPxviLiZ6YSkorvL3n+Fl+Z21z4+kORZ6eB/tSv/QVjXHi/Xp8hbpYAe0SAfqc1hYpK9+jk2Bo/DSXz1/O58diuJ83xPx4iSXl7v/pNi1NqWpXJP2i/uJM/3pDiqpweoz9aO9JmvVhTjBWirLyPn6lWdV81STb83cTAzxR3pc0lXYzDFGKWloGM20oFOopiG4pCPanUUAPimliOYpXQ+qsRWjBr+s22PK1GbA7Mdw/WsulrGpQp1VapFP1VzpoYqvh3ehNx9G1+R1EHjfV4j++WGce67T+lakHj2E4F1p7r7xuD/PFcHRXlVciwFXeml6XX5H0OH4tznD/DXbX95KX5q/4np0PjLQpcB55ISf8AnpGePyzWlDq+l3IzBfwP7bwD+VeP0nXtXk1eFcLL+HOS+5/5H0mH8RMwh/GpRl6XT/Nr8D3GG8mQZhuGA/2WyKvx6xeL95kk/wB5f8K8CSWWM5jldD/ssRVyLV9ViP7vUbhf+2hP868qrwfJ/DUT9V/wWevDxEw0/wCPhWvSSf5pHvSa7/ftgf8AdarEeuWwOSssZ9R/9avCU8S64gwNQY/7yg/0qdfFmtj/AJeUP1jFcD4OxCd4yj97/wAjqjx1k8/ipVF8o/8AyR74niWNF+W8k+jLmrMfiyEffdW+qEV8/f8ACXa3j/XRf9+xTf8AhLdc6faU/CMV6uHynPMP/DxTX/bza+5poxnxdkUv+Xc38o//ACR9Gp4r00/fOPof8asp4i0hx/x8bfqM18yP4n1xxg3xH+6oH9KqvrWrSff1Gcj2fH8q+gof29D468H6x/yUTzK3FWUf8uqNT/yVfqz6rXWdKYZ+2xj65FdBYzRz2MUsUiyRsPlZTkEV8VPdXEhzJPI5/wBpya+mPg3fG7+GlpEzZa1uJYD7fNuH6NX02Bq4qcmsRKL9ItfnJmOCzyljq/sadNx0vrK/6I9Frxv4+6aZ/COm6ooybO72McfwyLj+YFeyniuP+Jmnf2p8M9ctUTdIluZ0+qEN/Q16k1eLR35hS9rhakPJ/hqfH9JnawPoaXqM9jTSK80/LC+OQCKntrl7O8gu4zh4HWQfgc1VgO6Eeo4qXFdCfU47uMrrdH0RY3CTQRyxnKSKGX6EZroNGuPs+qQufusdjfQ1554HvvtXhe03HLwZgb/gJ4/Qiu1ibgEHB7V6O6ufseFrKvSjVXVJnZeIdMfWPDmoabE2ySeFljY/wv1U/mBXgUXhTWdKubSe11K0nuCxMtvOrLyDklTyAPqOtfRNncfarGGcH7ygn6968J+IVnqVr48urTR4LiGa4Vbn7XI+YQjZBXBHBznBBzXyPEFCbgqsLdU7rp0+7X7ztTS1ZsizUK8OpTWjbwzSMPnY4HGQGwG55P8AhRXn19b6/NaeRJqyQxW6+ZPHZw/vG4/hds8c84HeivjYZZUrRU6c4xXRav8AFIpyXSNzP8UaBZeINRt4r6O2ktoXyfPiDFSQMKrdee4FdfpHh/SNPtLK1kj+0pkNHAWwI2wFwsZPoAMc9BXDWGvWtwlu15OPtJiXMmzcEY8ltv5DPavQNHEk6xRzLDM7/JtYbwxzwwPUEdx+XavRwuWwhQjSqybt5ux89Gu5Sehe1YWVhbhxBuRDtjRV5bIyuP5H0waijvPMijiuLWKQyKFaIqJFI5zu4x0OP60zVL2IXtzcX0qi33sqiR+F55xn171zt74x0WwK/wBmwvdsAfkPyRA9j0yTXw9SLhiZxwV7Xev6X6fm0d6qRWs2YviW+0nwffX2k2AKrfWwcWcrF44GbocHqODgZ4zXm8uswPdXf2acWupXbLI8kXCxKv8ACo9Dx+Arpdbvode11tZu7KBrtlVM8sqhRgYBOOgqvElrvbbbW5djuJ8leT69K/QoZnSp0owacnZXfmeNVhzzbjouhnRal4hskXTbO+SSCf576W5USRyc8R7W4Ydz7n2qWXRNG1ItcCxhtfJjd/NtwYVLBTgAdDzjoK7DRdP1DUZmkh8Ow6so+aQiHB/76GOamSDwsviNZrhrzRblrZ7Q2t8GEJViD8rMAUOR75p/2nCpo4uKfW10OGHn0enY8ig1m7tgsVwI8bVRVWMbUA9DjOfU967DSdcvZfDGoqJ5DFCVnEKynaVB+cbc9MHP4Va8UfD1pbZpNPZllP7xFZcg+uCOGB9RXntsdQ0q9jtLqFo/lkRxn5W3Aj8eK9+iqMofuTza3teb94zstE8SXd7rFtbSTLHaAiSY7sAqvJJ+grodHXTNdvmuLy0t4b+d8QMZDE8nHG4g5bOOmMc9a80snFhpIhLAT3QBx/cjznn/AHiPyHvXd+C4n1DxA10iIzswZxt4x6A+lZYhU1GTlsFGpKMlFansfhDxBpKaNNYx2UmnyJGTEZlKxSMAePm9T+frWHfeL7LRtOF9bpJG8kZ81GDm3C9TtbGAwPTnA5BPNUdZ0vVpr1ra1vLeys42DIU/eOz53A4HQKegzTvEngfRPGt/PPcalq9t9sIlvLaOYbLp1HABIJjXIztHGcV8zVzHCQUadGqlbsr/AHdLn1FKEpR/edDK1DxVoGq6CDraXV3DqsbJFZCJJFlY8DYwOd2cdK8Mutb0fRNTezhsJLYQMY5I1yJE7MDnGD616+fB3w/l0BdHtUu9GlSfdZX/ANtbzBcfwgbjjJIAxj8q8mk0Hw1401V7w+Jjo2qsrNdrqS5W4mB5Kuv8bcnDAc969jL68cRG6vb0t6d/+HMZYZVHdPQzrfxPanEslzMCxIKxtgxqD37HIr6D07Q/hrJ4cstVfUVHnyKY57lhJOhAGVRScbgSCcD+leIRfCDVZY4JLaWCXzWwUe4RJCCeNozyfoTg111z4K8aaPpunaVcadMtkZ2W1so2L7XxknaO+P4qjF4OjWalCbTXRdfUVN06ad4tnvuhafO11LHp8kF7CGG57dQQcDG9j/Cx/TpXMfE7xLqHhuwttI0XUrVZbnJuDbgs0QGMLnjbnuRg1zdt4W8ReAvDx1TTNSNtd3jhmk5Hk8cx7ehPXnvXOazcRaxepDeSCyuCcqZTthmP1/hP14rwMFhKFGpKSu6mtr6K3kvw1ubYirJQtBWZXHxO8XixMF276lIMgSj5SBxtI9GBHXvXGSQapqtxLeR6J5ErFmeSFNvmMxyS3bP0Ar0LStEferfZyQeQF6H3966VLF5N6eSueoAXBY+w9ac81VGT5YrXc8uU6s1yzdzzjwza6zpOoJcrGVkVSoKAlsHg816/Ya94ka18xr2ZGjZQgRz6ZzXJXdxPb3ohE7KgVZEaJtrqO+fXv1rqdHWaTS4zPcGbksrhcHHbP+Nc2ZVFLDrESdp3VrdgwzalaJzmv6Pa65rEmqappVnLcygDzFgVcgcduP8A9dbWia1qfh+Ippn2aKHgFfs6KG29MkAGtG+gjeKSX5UO3cu3gDjofxrA1kRWGmPKZdpC52dS/rgdq8mjjq8pJRm7vzZ1NuDumep+B7/T7+xvLgsYbyEvdyq7ktK3LFgT26jHYVW/t43czXTbleT5goJ4715Pba7cWtw9xYK0CsuD5nOQRzxU8F9qc+nyyT3/ANjjYmNdpCuw+nUD3q8c6uIoxpydrPV337fNanRHEK2p6J/ac95dTJb6bK5WQAyPGSJCeSV/vfXpXnXiH4YaPe6jNc/2Z9ilVjLJJHcNGXZjnOCTnn0HFUbSefTrf7JZX08cOc7VmbBP51ZbVbqJQ811sH96VqvDQjhL/VW1J9b7+q6mFSt7TdHa+G9X0zwPo8WkWr3WoI6sZ2lHGSOAAxORk89OK5Syk0zTNfm1nSbOHT7uYFWW2QLHH0+4vRenauj8K+FrLxXp7akNfjaJJPKdIUO9G64bPQHsehroLj4b6HsXZqN9AxGAw2Oufyr2o0cxqLnT5fR2LUZcqS2MW5+JesaP4euJZplkEksSmRV3C2hyfMIAywfBGN3HevKdT+L3n6pKiR3FzYs5Tdc4JlXP8S8jp25rR+J3wg1+3dvEfhyQ37sAt1bou0ycY3IueGx1UHB7Y6V4zpfh3V728EMmj3zNEu6RfJZSDngcjivcjg4YmlGGJd2vvCrOUV5HrGl+MtNutQWCJbeKJySwtbdWkXj+FQASe1ds1omoeFp9X8Eaxc6q5H+kWd1EY5Gxz8h6bh6AnNeY6DZaX4JDazq2mw3OoyLm0tnjy0JH8ZYnC/QAn6Ukvxb8Q2d5Fs1LbaRKFSAY+Q5yflHyj0FRDLcNTv7OPMznU1Jan0x8MvEGm3vhE2Ed6Lq6tAS9v8ySwBuSrK2Dwc9OKxfF2paDuWyl1UreRYws4byc54YoAdxHUNjtXlOt+LfDfibSU8TWNxNoGqi2kIaGcCaMIyjfIwADBsnA68elcpf+PdMF7NZjSfObcN2oNOwmcbRy2cqzd88dadaFaUfYqPTz2Olzajbse3eK9H06T4dXsaXtvNaNEZLa5ikDlmU7unXdwcjHrXnHhrwmviWGKSxurO0ntMYe7VvMnJ53YHCqO1cxfSsr2M9rdtLaXcIeJwME/MVbcOxBBH4V1UV5p3haygu7m2vby2eTy7u8hB8tATjZ7/nWNGl7JRUI2X3+pw1JqrUs1sZWvWVnomoxQRWts15Gv7yQZdcn05IJ9/Qiqtrr93Yh91ss4RQhEQOyJM5wfTvge9e4eH/C3hzxClxrWlajZ6jpW/MZhIeZsrgpJkZQjP171UuPhd4fi0uZdPF3bOjLtaS53K+TwrL8oIIzz2967alFThyyV0WsNVTujjrn4x6loWgJpGlOVt5I/MWVGG5GJ5A4yOnTjrXDXnxF8Q6x51tf67JawXp2szdCo7MQMlc9a6zxl8KrxY216wJuLRWIuLYHa1sAucg/xKPz+tcqnwg8YXg2S2VvYKV/dfb7hIpZsAnckf3j7DHNKlh1CmoNvRCl7aTs7/I9v+Hvg7wxa+E9P1G5trW+v7mFZZ55kWTcWP3VJ4AAI6Y9ea0dWg0u/wBLvdE1C3BgDbYvSHBJBU9e/TpzXBaT8QfBelLFpFzPLYW1lAiwPChmDEKFwF4OD8x559adZeMoPFK/2XcT3OjzySkWuoIn7qZh0Vx9COhyK+exP1+tLkpJQV2ua+69NbLsd8PZqNkWtHm1XQ7byNHtLd1ExZpoolEoQAbW2tjnOc813Ca5c6lZxx65JLcMvLeQ4GSRjjjgYJHFeS67d6npV2LXT9eh1KaLe1yscZPkhVyMHod3IB9jXAaD421211WWR7oeXKpLxyDduABP3uoNZLDZnySUKiVul7p+T3X4EqapOzPTfGHhCe6judY0tljFuAZVkbiNCcB2PfBI+tcp4b0/xBo94dTTxe8LxtkJbBhnB/iJxx7YrtfB/irRdd0mQTLtkkLRyIMjaOMZ9eefwq6/hnw7p2lvq2om81O2t/8AWAnj67VIz6cmssJiasb4OXuSW7a/LQzlC7U4I5vU10DxDrtrJryLZqoZnNnGES4GR80jjAH4DJzXsOiaX4N1zQJ9DSO2n06GEB7VF3KyY6gdue/XPesT4m+CYta+FFnrHgzS1S8sxFcwQW0efPiYBSpX+IgEEfQ15p8NfE9z4Nvbo6nptstxIxt5oUJUjaeRgcdcdBX0McudGpCUqjlBLZ6/Pv8AmdGkH7636m3e/D3RvDninTr3Tb+90/RhcGNyZSz2kwXKTr/eXGee3PNeu/Z9K8QeFW0m61AeIrbaDNIdhZ2U5DbRx1H/ANeuVi8OeMfF2oT6tqciNYnelpZ7RDiJhjDcZHB6evOa8pt2l8G69dNpM13o9tGXtZreUmZ3PRlXd0HvzRVoyg/aRfltuv69RxlGmtVoyW98IaX4m8VPa+F5XEyo7pFcBzA4QZIQZyM9MdDmvXfD3hnS7/SxcXmm3NvbX0atJpEiEJbkAZCKSNo3ZJUeo4rgbPxH4au9NXSrSz/spo42jifeVKMRwQ4Oc5965nSfFnjPQLuDQZ9bkExnV2WRTK8iNgHax4xjP0rz8LiazhJVaTvF3tLt5aP9SU4Lre/Y9A8ZeJfDPhrVZPDl+97bTKsckEiW6IkMZBwFYYOPoOtckmn/AAs1tIZtT2SXzNljp0x3Tjdg/KQpzg55Gfc1g3fww8VeLfEuo6tcXUEcUju8QWTc7jnagB4XsMnpXCf8Ijqsevz2etabq2k6dC6wyzNas/lMehdgMYz3GfavXwtbD4l81KSukr+XlcycJXu1oekeOzpXh/xJp9p4OebShBAkpwxSaNs8E9wcYPPNaK/HvxlCghv73TwzBo1nEWZB23MnRj+QyfwrUtfA8c/hgWR8Wi7xxMbksmxMYD4f5lZQcg556GvMfGHwp8TeHQuvSIutaF5YK3ljJnd6bh1Qe/I960wtZVJydGWnkVKnUh70NmZmtePtW1nzLd0lmlLncVTaHUHj5F6HuetNfW9X8QWsi6jI0kelQ/OwbJ2E8D8CcZ+ntWZYeIZvDyFNPsk1BD/rHkQq3XoCD0H+PFZUc0s1xJe6nG6+cxYIibMqf7ue1bywGHjBckUpfl/w55rlPmaktPXc9h+F3g7wh4xa/ivfOju7cCRAsg2bcdT3JB6jjivV9Jj1zwPMNHKHULHBdY7VjJJGD/GoxlfXB4NeCaB4z0TwxZp9hi+yXm0+c4JkaWQdMHjHHHoCe9b8vxv1TWNKKM8lrOjt+4GMSoVO35uMMDjPGCM9K87EU6yi0ot9nex00ZRSutGvxPcdR8T2t7LbWywGzvR86mbgTIejKD15wfUEeornNd0qLxMklpqcLXbSoVVlYq4wCeCuM45rwy68aaqTa614mulkk+/Zw28ZCjH8ZGcLkjqOoFT3HicXniOe3u7u8ttUM6tY3kTsY1jxuVcDpuB5brnHbNfO08lrLEKfN5trut7PT5u33noOcqi12PT7Dwbf6bqyXup68YreBx5dva/J8gOQkjnr7jn611M9r4X1m7me70TT7tpkIeK1T99IB/FjI6eoGfevO/DGuXHiTUy+qaBL4wtLS0dooZN0atJ1Rtx2hjnCnd0HTOOd648cf2DbNqHijwQ2hXdtEGt7u3UOIWHAETHcoPoDjPNerSwyWK5sVXTlr7i0fl5syUUlojI1rwxo0NoNC8P6xrwtL+83YMO6K2453PgZOQB1zXkOt2V1ptxe28unSz2UExQXJjbYzdMiQqM5r0S7+Lviu4t4fJ8K+dbXjN5Us0JQ3aMcE/LhV78r3qzqWn3ut+B7zRLXV73SYztxYXVwLy3I67VcZZRkD6V108RXo1vZ4m3K9mnf7+q/FeZM8PTau3Y8KjN1Jfx21pCxdxjaFyxJ7ACvRbVCNItk0qaXTtehj2Mzh28okkNsZFJXjsx43Niuz8GfDHUoLWOS0hRpYFxPfK4RjnnAyQSAOmK3ntTp8s9isqTeRgPs5Az7jp/jU43HyjFThTur6Pv5+hNlH4UZfhm68W6bqVvJcaskcVsrBWiIc3QIADM4wdvX5W+b1rs7O70SzhlabQbG/vZZTO11qC/aGBJywXPCjGcCuZkBtrhLhSfJ/wBXKhGQAejA9sH9Ca05YljSQMORGePSvmq+a4pzU4TaT6LZfJjU2jnLfwv4O1DxzNrN34aeKOZ2f7HA4+zHOfmK46dyAQM+ldAvw38BWGoG606yvWmx51viZ3jjbsGAAK7WHTPTFZAvDauXiwjrjnAOR9PSnjXp5nVSAm7qQ+AT688Cuujm+Mp2v70fP/MlShJWluV4fDniXwTc23jTTNZgWBpDK9vbs481C33VAB3A89Rxjmtrxz4l8Patc2c+q6VHpGtalarLaatcI4CRbsMJo0+YHggcHOfSsbxNeajdeHntY9QurdHC/aEiYKZ4xk7GbqF5J469wa8j1zXo9XxNHpl2stsqRC6lu3nZ1AwqNu6cdMdhX02CxixdLmjp3X9foDtCLjE9ZvPinc6Hp7eHNN8SxazY3MRW68+1McO454iLHcvGD0AzyAOa5fV/DWq63d2aiC4kgmiBY22ZI2YcDYVO05yDx0ya8yli+0um8mM4wo5bn/65ruNG8cT6Hp+peHdIZbB7NQzTmQv5zAhJsg9M5424+6PrV16Um41E9Vp6IiEvbRak9Eayw6Xpthc2l5qrfbYhsjgiLCSIg93wVOehGfxrv/BviHxBJpDTataW17pUbHaEiLTr06EdFHX5uteXaE+hzESSW10syLvaeeLcnHPQDH55/DrXf+G/Gsl1ot7sigsxaktGYgEEhI4T03cAg9u/FcmIoQjTtU66bd2TTck/d0PRI9OuONU0a8aJ5cXEYEbQTRjuDxjb9ePc1uS+M4ptGWK5ntNSlYbZ4kkUOCDxjbuRvfkfSvnzxr4l8RXVjolnq9xJb6VPGWtE88SmQA7fmYHnkYGTwK4K18R3C3ItoFNtGGKbVJG8D1aiOElQjKGFfLf7l8r7m/tpLWx9I3uu2dlMyWqTRW7kLCso7nqufY5x7Vj3WtX+GitrZVbd80snzKB0zxwAPWvGhr95awNHHezRpuyVBLA9w1a8XiWaysbC80bUrgXdwG84hzu3Agbfp0OPevClk3LUdSouZyfou/y/EPaXXY9DEd1cgkRPNcA4AiYMkh7kEdfpVVJJrbxBDp012+myTsqNtI3Ix6r7c8c1d8PeNdA0+8Nj44tJUup4lc3dkApgyMguB949DwOPevQtO8BfDrxbYv8A2VqltqEasrxNCwE8QxyGYHeSTzlvyr0aOUU6iv17f1/wSoRk1eJD/wAIpq1lHPLpmrW8t264zdxFTkdm2elcJr+hiOWzXxFpSW0xuFMLod0TMWGdjjsf7px9K7vT9QurHxnceENUnU6nCqm3kn3ILyMDhlZf4sDkHOSK6yfTYr+0msL2HzFk3ZSYDL88Er3+o6exrwY4SvSrSq+zsk9Gt9Pv6HdywnGyPB7HwrLrso164sZprFZmaGC0iLeY28hnJPygcY7nAFdqLTTrGQLeeHZpVlOElaaTBb0bpj/IqfWLjXPC08VrKxktE2fYpbR9iQx5A8uUHsvTIGSD612ulapamyac6vFcspAcpwpJOcAHjp6nOK9lYWnXl7SvJveyvZLy0OdRSbS3PNvH8GjWnhqxOn2RtZ7xw2FdsGMDJyCcHkivMxxXYePL59W8WSrbIBDbKIlVBgZ6seOOp/SuZ/s+6wSY8D3r3cLChhaXJF2T137n51m9WpisZJpXS0VvL/g3K1FWXsp413FcjrkVG0JCbs9OtdX1ilZNS3PHdGorpxehDRimvLFHje4GelSKFYZDAitVNN2TJdOaipOLsN/CkqTC92A+pprAAjpzVcyJtLew3FJipAuFVmIXccDJ6mnNHtGWIUe/FZ+3he1y/Z1NHyvUioqUxAY561N9gnxny2A+lL29PuV7GpZPlepTpas/Y5ugTJFR+S+cbDVxqwlsyJQlHdEVJV9tLvEjEhiJX27VXa3kXqpqI4inP4JJmkqFWHxRaIcUlTiB2GQpNC20rthVJPpV+0it2Zcsr2sQUc1YaznTloyo9TUbIEXcxAHrQqsXsxuM1uiOihSrNhXDH0BpxCpjeQuemTjNPnj3Dkne1mJS0jPFGu55FVR3Jq/a2aXMStHIHL8rt54rCviqVCPNN6G9LCV6ztCLKVOFbi+H59gcKWB7ircHhW7kk2lCD/d71xvNcKlfmN1l2Jb5VBnNUGugu9CmVwsCFz0xjmqz6BehMgDd1K1cMzwsoqbmlcqWXYlScVBuxj0Vdt9OmmuGgKsrqMklePzq/B4du7hlEW1gTgkH7v1rSWPw6dnNEvA4lK/IzCr334B3gfRta08tzDcJMB7MuP8A2WvL5fCEyrw5yOpPSuv+Gl/a+DdS1O6v5s21xbBVKHO51bgfkTSwucYT2ivOx7eU4LE4fGQnOOmqfzR9D1Fc26XVvLbyjKSo0bD2Iwf515a3xXtnlMgZIrUHkv1B9KS6+L9iiJJDbuYz1b3/AKV6FLPMJVnyap+aep+guUWtz5zvLGSxv7mykGHt5XhP1ViP6VVYYrrvEMKax4r1PVYpFhtrqYyj2LAE/rmsfUbHTdNsluLq+HznCbOScdawnj6Mans0236M/NZ5ViVNxjHTUz7U/My/jV0LVewbSbh0ki1SEDOCHO1h+FaDTWa3T2zkpggKw53k9MDrWkswpQfK739Dm/sfFTbtG1jtfh3d7J76wJ+8FmUfTg/0r0+GXCDNeI6dKdG1tJoryMSIpDIx6hhXZR6jq0sAZsojjPXBxUz4go0rRjFv+vM+2yZVKOGVGsrOLf3bntXhm8RrWa2aQZRtyj2P/wBesr4iWtvLodveXCu8cEu0hGPG7gHj3/nXnOk+J5tKvTJKrOoUqcfpU+t+Nhq+kXelPC3lXUZQtn5kPUEfQ4rysw4hw86boNfEumtn5/M92M4tGBqWu2diXna8jtp0UlJNp3Mf7o28fnRXNWnhLSHu2fULm5vJcEGNvl2H1or5t5jgKfuzTk+9rfmxKo+xk3fgW80KzRr/AFa0KqQjKh3M5HdAOo+uK2U8Y3dta21tY2MKvbAhLmX5pOfpx+ea8nuPEyzQpd/bGMdyoKFSQVcHDAk84rPtby/XW7e4hbJWQS5PzLkHPTvX0H1fEV02nyr8T56XLSloen3d7cahctNeSNLIx3Fic5J6/Sq0tvKIw7RCKJuhcda4fU9Wvku5J1OHlYsccAEnJwK2PD/iJXkSC4vXgc8ASfPGx989K8qtlNalHnhqhKrBuzZuCONgFQh2PGAOtYV34ttvDOuwvbKmoyxZZ7bAZE9nPQ/QV6ZZ2Ok6lot4+oaekEjRMHMRO4IDzIuOR/8Arrirn4J65qd+urw6zp8mitgp9jBG2PsCD0P170ZVToVeede65eh3QozVpo+pPC2uaT4i8K2OqaRDHFBLEr7EAHlnupA960Lmws7xPs+o28M8T5ASZAyt7c15V8MbqDwuX8OM+2JzsXzD8u8en1r1Aq7SgyTbixOI16fTFehhcVHEU7x1s7f5fgei1f3rbmReeCdHgtnGj7NMYDIjyTAT6FCePquDXjXi3QNMv7bU1uLUadq9ku+5RjuDDBIcHuDjhvwNey6zdto1u9xdXkboWBjjJ+ZfUe4xXlviLVYdT8UNrlvHPA7WhsRuIKSLyRkeoJrixWJp0Jc9J8s0/h6Pvf8AzOTEU6c48skeeeGPhq2pxpq+v3ZQTgOltF1C9sntxjgV6dbaZYaNafZ7KOOGNeu3v+PUmsyDW4o9KS6hgaSFMRlk5AYdR9au2Cza9dwW6TRafFK3z3E/zGMeu0d/rXymMxWMx9W1aVo3slokv67sijRhB2itSwZZZDtijLsBkYHWuO+IHi7VvC3hiO6s7aXzbmURC424WLvwfU4wPxr6B8NaV4d0eXbYXMNxMUAlkkctI3rz0wTg4A4rivjP8Or3x74PEfh6S0jls5TKkUqFWlIB+RW7Dk4/Cvqct4cw0rVKlRTa6KzX3noeystz5fn1efWrSKOadyfllj6fu5ByCD1Jz3960dL8Mx3WtXevzx+U1y3mPCo3Rwu3LEn/AHsn2zUOiaNd6FbxjXdMnt9SSbYlncR7cAfxHnJzyMfjXYmS6ugqzv5cTH/VRjavvwK9rEYxYKPIjw5KcJOPNuTaNeDTdThniZLieEHY8vzKmeoANepaf8RrxrUu9smIiiyM/wB3BOCQeo4ryeBUgZm6qo+6B0H+Na+nX5S62uo+zSDa6nkOD618pPGYhVPaRb/roa0akoOyPR/FniC1N3Po13BEJo5NrzQgASAcq23pnB6ivNdd03wrf200ZuRPMqmQIpy2ax/idqOji3txp98s102EmEbFvLVQAvP+eleV2Wotb3y3CyZCsCfmyT7fjXt4bAzxWJeNba10i10W1ycRWk7xZ0T6zrmhWvm2tvMsP3VAVwi+4p9l8W9YgeOO+RZ0Vs/N8rIfYit/XfjVdLoa6R4eshZQiERrLKFLx+uMZB46HrXi08nnytM5LM5LM5OST3r16uBw9dXq09TmhFLZ38z6I0Lxd4b8SySsJRFeiNQls6BGbbkn2fr+lT6t440zQ5xFNI3mAArHEQXH+8O1eH+G4IJikyXPkTwzjy5N20qdpPX8Kyru9aSZ33FiTkk8kn1rwp5NTrV2pSbiuj/zOpNpI980vxbP4nt7630horeaKMt5V0BiRO5BB4xTxf2ctu5mzb6lFhXjkbLEHqyn+IYr57We+CRmKVlyc8Dr7V1cWpam2j71YCSK2ZEcN8+3eBz6EZOKitksKbvTdk3/AFr1RVubc9LvNWsoAWaRUAOQW5OP90Vy2p65dXmUs38sE/ePLH/CuF026zfP507l3UjDDlj6V1EU1tkGSMgE846mvQw2WU6MryXM0clWThojQt5HRs3FxJI//PINk1um41DVLNg8Uj26gKzmPAz0AJHQ1zV9p+ivfIlhr6yTSqGW3Y7ZFY/w46VQsLzU/wC0PK0+5KS79giD4Y4Pp3qq+FhOPtFpYiPNfltc9Z+H17e+GvFMU6NK9lOhS7i4xs/ve5U8/nX0NapZ3sMbIySxzpuSVGyjqfQ1822NrdanIz3njCz0eK3XbMsduX28EMXJwASM9M4rb0L4k+FPDc0dhoEt9qFjtCGDBePPqGOCD6kDFceDxk6a/e+8m+ibt66HsQj7OKUj3K90tJdNubCRt1tOhUSq2TG2OD+BwQfavHdM8RSXFql9PM0t/cIYmhdsqCmRuC9icH8q7xPHWgz6W+oTat/ZsAQLJb3I2tHkEbhjrk968j8QXWl2niWDxVZahHPprxxvLFbjBjuCACOePmOW9OTVZjSpYxQdOa7ednuvyFUuldHrehSS3sEam2MvmYkYSYcxkjHGa0bnw7pGqWLxah4esrtThv3kKqwORkk4/wA4rzLTPHtwQdljHaRkACef5yoroJ9Ku/FMEkUviXUw0nyKbefyEU+yr1z6k1phMTzXp04aLq7W/UyU1s3dnlXxE+G/hHR9Rng03xVpNpcXR/5BmsTNtQsc5WRPu/8AAxgeteR+L9O1Cw1GKe50uPT4pI1jQQP5kUjRgKzK+SGyQDwehFemeKfgXqrz3N7a6tNcOqmTbIPMMmOoDdc+xrlbHRY9L06W0vbh7u1Mgka0OGVXA4YZ5B7HH0Ne/Tckld3ZlUr00rNWMWw8aX9to9tplsYVjsm3xz+UrOGZtzLkjkZA4+vrVRr3XtfmmuJZru5lkbcUDNIAPXA6flXqGnWfhf8AsdtUsvC9o0duqidTGWbB4JwScjPf3r0nwtrMOi6c8GiaFBYeYodGt08vef8AaHWuDE4qnhU5zjZd9DP3ajsna54v8NrLxZaeKnfRdVn0eZLeS5ZnU4nCDhCh4bceOff0ren+K3i3TvGV1c6zrcy6ckLPHZRIvlTZ5RGQggZJ+Y9Rg4r3K/u9HuLKC51jRZTq97AyLc2q7JH2/wAPmZAGM5wTXg3jrwHq0kkF3pOgX9zbOD5kkkQEy+gbaxDj/aAz60qFZYmCrJ6Pb+u50yvTSinc0B468Z+MND1K/wBC1S1iiSYO+n2p8u7twq43R8Z2Y5JB+tedG+vWu/tEt1cS3bfMZ5JSZA3c5zn8c17PoPwj0nTvhNfXU3iKws9WnHnG5lcrHARg7GzgoeMZ9z1rkvh54A1DxPLLrL2sWpadaTCGW3tbhfMnDA5KnsBx7nPHSrktHfboXOlKclyX1PNpIZZGDqckNna/8XvmrdlqdzEUlt7pZAr7gQ+QGH8jWp470VtI1G9k0dLm2iS7Nt9hvFxc2h2hhuHRgeQGHXFcta+GNVttSMjpKxeD7S0VuPNL84C/LnByc+wrV0tGpNGMcPNq76Htmm6va3F666gWNzeW8/2eXbkXD7DjJHAAXge+arfDz4fal4p0k3iaZGliMhJZzsMx77TjJH6V5xBPrWj6ml1ezXWnQu8SJYNIwBwAC5X6fqa+vvDd3FbaPaeVJsiSMIozgAAdADxXnvB8seWct9bot8kpW7HL6V4J/wCEacTanoDT20a8fZ2yo+u3npW4uleGNatbnT9Hums7tjhY2m3LLxnaQeldlHrSs4jkeNGcDbCzYk+o9aw9b8PaTrjSTGJtPukw8Wo25CMjdj6Eg+tcby6EdPjT77/KW50qyVkXfBHiC0tdFm064H2UaY3lPHnJTnt+Pb3rl9VsPDlx4vk8R6VpVva3RG0TbcsTjBbHQE+wrhbSw8aaH4hvtQ1S8Gr6bffLNfRx/Kzg/KeOh7HtWums2qw7rcNNycY6Z7189mOKxtCnHBU5PlXXr1sr9inVTSud7YazNaqGl1AqCed5rzrxX4F8S+NvEd7qujavYrNJ9yJ9xKqBxz0B/wAaYTd3x3TSYVeihvu1ftJbiwYSRSurdmRiK4MBjq+ClrPmV7tf5X2InKNRcsloeHz/AAp+KDXzpeafJaBWIEksn3sdwByatMvjDw/ZSS3Jmaa0OyCXUbfcGJ6iPdn0znNe0Ta3IZQLi5bcemTkmm6lrO7w5Jcy2BvrQA74iu5hjvivqaWf1MRVVP2Oj8/+AYKnGKbizzyPxH4u1zwnpsdzZyyTrJ5jzWQKiQA4VW2jCke3Wuktj4ovkgF/f3ltax/fguG8wY/uhT6+/wDOvONV+Kerw3MkfhlE0W0cYaOFQu76j1rrvDXj+48UeF77QL+ZDqZUPDcH5TIM/MpP0ruzHCyp0eeglHv5HPTqqc9ZM7GPRdF1bRr2WbWGvrKdzOloiqHglB6JJ12Z/hORXFa9oniBbKHwlpk+oNpUl01wGSV3wHXB8w+mR06c5rr/AAr4Rj0TTZ5pria6uZxv2o22NfYevuaseJfEWm+FtMlka4UzbcIgGSSewzXztLNXSrqjh26l9EehJSnBc7sc7oXgPwP4TSP/AISSY6pdS/OscwxGjegXv9W9Kh+InjnwhoGn6faT+ELHWLt4SYpGAjVecEbVGAMe1eStr1xc3pe7LypksSXJJH19qe9hPrnjXSLO6jdLQBm83b5gkRTuI3D16e2a+6nF04OVWWiV38jghUTnyxSt94aX4e0rx415d6XaX2ivEMosiCa1JJ5VXG1ge+MNXTad8JNGi2yavfXWpkADywxiQepGOcfjXqVpa2tjp8VnbQxQwRBfLWJcKgPYCiWM7CYo/lyQQOi8cV8Nic/xFWTVF8kfx+/c0dlsjiNX8A6TrVxGRfPYwW9ukMSBA/lquccnr17g9Kn8KfCqG18Sab4jutXj1M2VsImjVcL5y5VWIJzjZt4x1BqXV9S8stawAGTG6Rc4IA7f1rj9Y8RTpKsVhcPCVbcXQ4JPoPQVthnmGKh7CNSyafRbeu+ovrahufQjzzRIqo4GOQF6AfSo0uEd3guIVmhfKvG6hlYdwQeDXD+CPEk2u6bJb38+L6BcEngyL6n3rYutfii1W10vTI21G7eQCbZ9yBSeSzevtXyUsBWjXdBr3l/V7nZCoppSTPIvGGqxW2r3mjaJGlvZWsxjhjiJCpzzx3+tZP8AbdxGixxOwXG1xnBP416prHwJvIPEF9qOoeMdNt7e4kea1hGWnmQ/NjbwA3b0965G88MX+lWs8dv4Hv8AU5Wb5JridSoUdCEjIyT7tiv1mhKhhIqjVklKy3auzyq9CtKblJaHc/DLxtoDabHp3iC4YmwLNCjqTuB5BGOpHIwfqKutqE2oTXKaT4PTVo45GkNxMvky4Jzjgqfw5rxCS78R2cl099obaNEwVAkcbxBD6g8kn8SKuL421m2s3tY757iM/dklyXH+6x+YD/OK6o4eM05R1i+1geIlCKg+n9dTsNW8eyeTJaWmmWcZYFXDAuyDoQCTn86w/wDhMdZ8jyvPR02+XhkBO369a4OS+uXdpGk2knOFHFXpbuLyrV4pSssu/MYGRwABkfXNeFVymFO14p3b/wAwjW9p1OkfXr+QZeGF1HQ7cE/jXW+A9Hfxbqj3OqThNL0/GYiwVpnPITPZe59e1eVW4vGiM4u3O3jaZOfy9K2/DPiG+8N66mqwky7jtmiP3Zk7qR/L0Na0sBGMuZxTRUKkbq59SHwp4Sltmkl03y3YA+Z577gfrkiuI8b+A7HWdCmSze0FwhSSK6MapIyc/IwXG7vgj8RXQaPr66tYxX2hzQT2ZUrIJJZC8PfDIQ3OK53xvpV5q2hLqWkXx0u4srhHVLuTy42APBzzs+h4+laV8DSnFSpJRktmla3+a7o75NW2PIvB/gJL3UYNb1U/Z7eN90QuWJEwB/1mwcgD3JH1r3vSbDQNIXbZ6TYKxO5pI7VCzZzlsgZPzV4Vrl7q0Piu4sbuaW08pvKWNxukdM5G32IxgjtivRfDN0sMoK2aqQAEa8tpHLccgliP0H4V0YfnlFVKm7+5dkc8ZWbij065vTqNkkccsEWzJZY5xu68Yx3x+pNeX+NvA+j66DHDfQadqXL2zzAQpOT/AASDAGT2b7w757drcahdW9iZb7RIrCDBP220hjuYMerDaGUe5A+tc3rGtwWOg3FzqOp21xaTL5aJEGZH3A7SEYsOD3VvX0rueiuzpeqszwjX7bxJp0EGiazE9itqHMEdyo2gOwLFTzuBI4I49Ky2isUjRLiYQswwzr0ZuvQ+tWvFWqalcX02s3trJcLLDGglXIjLBQN2QMA8cjjkmn6D4E1XxVaw3l28elW6DzC13IEYq3Rgp/hPYmqjCDXNY4XCSd76GZKsTxr9nV9ob+9ux+VbnhTUrfS9UV9SR2iHzo8aBmicdGAPHr1BHrXeaV8FZbjTzLDqUKnkosbCYyAd8qcc+nWtj4c3Y0DV5J9Pur67sbSPzLzT3t1RZHztYfN09s45FOtyyhyN79h0+ZSTa+88m1XVL3Wtcnkeb7TO8m3eiACQjgHA9QBXZab4e8V+FrC5vYNJuGllUeZMmPMVD2XHIB7nqfpXpfiq48Ka3qFt4l07SrJrZ45GiukZopmlRcmJ1VcEDnnrwRXjk/xCvrdp7OzaNZVJZJohkvnnljwB6ce3FcEsOqkVT6dfM1bdOblcl0u9MXi37TfaxqFtqNnOqxQJDvcOp5GSwCgd85zXtOnfGySaeZbvTUvLQSts2DZIqDvnof0r5euNYu5JHS7kVJ5mLPMrEySk9mfJzU2naxcwJFaQ3TQRGTDS7clFPDY9R3xXPVoVkv3TsjdVUvh3Pp/UPFvhjXp4L/Qb2YycefaXEe5FB4O7PY9OM59qTTPE/hyWz/s+wmS1WBXkNtGxkwM5Y56n/CvnXQk1iDxlKLa+S5tNsqtKhwsqbT0HUZ4OKoWur39lcLKtvGypIGXK/OuOwbryOuc14tTKZV5TjCpZNJ2Wmrvv9w5V7Wbse/P4k8J4fy5SwGSZBGTuPUj3NV9W1RLeOKMWckEV1F5kLlB86kcEg5wP1ry0Xc7xQCdgI0/1b4/5Z/eX8QDg/SvUI/Etv4n1DTozBEkwMSQQbxHtRQAcD3GTisqeWUoe/KUperMHy9Iq/ocnfvqbQQLPOogdQymJefy7ke9Z2sRWo06SO0k86/jZM4YhGVs++M8da9D1PXfhrbX17pd8bvTryJiGWVQQSeQV259iO1ea3fhmPWbuS88La79tC7i1ncERzsc84H3XH0OfavVo005R3ilrtoybct72uYSJdwzvHexvA6DPOePqDz+NawvtKtHCSltRQ4O63lKZHcYIzn/PNJBb+IbW6j07WdMnkg6xeZGSAPZh0H402+0OwZDJGFgAHmFTIuQO55xxXrToxnu/uOdTe0kaXiI+Gf8AhFtP1PRjdHzZGikSQkZYDPJ5wR3x168VjaHfmWCeFoVeQbfJc5OMnp1rLmn0pbGJbGadjnMqSgAbvUY7YqG0l83i3Y5U4bb3rneGfs3F9y+dPZHVahdSsqSzRLmEkPE/Yg44rNuNRScKGkkeJW4R2596q+VdzMEkZ27qSTwPatrRtMt7W4hk1azbyWPDbfvH/Peso0I01qNSV0kdb4Fn0uKa2XUYWkWVm8hZgfnHf64r3K68L6PrFhHc6fD+4mTaRGcH3r5o1m21ptdNpa2eoTMqqYvLVmHlkfLz0xiu+8G618SvDVtPCLRJLOVSBDdyKhVsYBU5yDXj1qVKMm61RJS13V0+jX6o66fK/dlEbr+qWWieKpbZ9IWKO2DK5BG5Y/Xnk9jW5Zrp948XlGHEuCOOvfNeL+J9L8V3N2bzUzcXt24CynYdwx0x/eHuK3vA/iSx0aaG2127EVzDykZyxAHZscA+1cGLwC9gqmHqczW9ru/yK9nFys4q3oe2QeBdR1Nmm+1R2TbDsGzO70BJ/wAK8s8WaPJ4evtviSQLdJhgEkYLtJwDx/Ic07xD8bdWuppo9DmktbbbtSThWHvjnH51x8mgeN/F8Z1u8nl1GFceZMZhNIqnodgJIH5V6GGwbpQU6z5Irz1fl/TuaTVOXwK79Dr9B8PaZ4mmjng8VWlmkbjfKXZSq+nPQ+meDXoOsaNqWjuqWNlHqQDhUmj8sPImOMjPPueK8jsPh5e2M8s9wJTb7QUuEXCgZ5LL1BA7V64vhTVtCRdU0mV7yyRUJi3iRrhB1PAxWuJdLF0eXDrmt5v8f6Wo4Qi1aUTgPiFb6/FYxsLLybI8zxRKS+PUsOB9B+tY/h3whqnjSzt/sswXS7bKSzzHBjPoCPvnH+RXq3iDV9Tv9KvNK017iE+X5iW837lkJ6Yb0qt4L8Q26acPD+rWMFjcxZZhFwrP1ZjgbST14rhWb/VcG6dOn78Pn03tv8y3Tp3S2OJn8AeGNH1AiTVppZTnZEZtmDj19fYmuXlntrO7eGawklKn5GmyM+5GT/PFezeI9QWMJaaPbKZZhlGmiBBP1OQK8i8TT68Jo01HSnsk7FFG1j3wRSyzHYjGtLEPfu7P5JWOXERjBNxO68P6FaeKtOkltTpK3SJzbz5D/wCBz+Yrn0W50/XRp98wtBG21gx27B7GuBF5Oh4kaM9mB5q7bXFzepIovwpjXPztgv7D3r2q2DjyWTOWNRStoe4WU0ElnGtrcrIMffU5yfXNXZIblQ5kDdMsWXP4nNeJafruoaRNmKfeDgMrLlT9a2774hte7TcmYMFCnncAB7mvkamUYjn/AHTuu/8AmdiqxS2O9nvre3c7pRgfwj5qzpdYWdtkMZPPGa42PW7ebH70MW5BzmrhvYYkAaRVZjwM0LAezaU02xe0ubNxPcOn7xsKe3SmWMNx9vSeETGIYDYU4z2JrGn1q7lukM9xl0GeRnA9K1YPEl3FCY7Yhd/8QGf/ANVejToKmrNaeQuZHdRRS38i6dfswhkXBB+XcD7jnFEnw70t4XGn2TWxhcN8kjEP68bunvTdOgu9e0GC5d5S8fD7D8xx2rVTUltl2M/l7BgSRk7l/PrXLVxX1eapT0jLtp877HXBRaucZq/hvbbzpHfeWsZyIpVBH54zTvDVreCFo5rmE+cdoyAUC+/qKj1jVrudpo73UftyLlo5eMlT1Gev4HpUPheWa6v7SyhlWGJAEIYgZPU4z3ral7epTlSjLnSaetlpbur9SHbm0O90vQPDem2BgRDqJIJPm42cnkbewrm9a8C+C9VkjgOnPYckJ5U77WJ9jnH4V3VnpYkbbHLDLhuF2kOuO5J707WLe0soGuHX5im7BGGJ+nalisTjsPFYhVPVK2/l/VzoUY22PEbPw0ltDNp1t4csdKSUMgvp7lpZ89MjI/TjrU0nwseRDcw+KYpriPG5LiFlBHrkE10+ptbah9mb5YjnqwyQPSs6TWl0qQWNp5s0kgLYC5+tdsM3q1PflJc1vhUb/i3+pytq+i0KcHhHTf7PeCZ2F8GOLmNj5eOw2mr1iuvWTiO41KG4ijAMbICGz2zmq1tfXd4jtlYlPTcCM1dQSLHmTn1I6V4dbH4v3lOS18tvTsTdbmBd+Lmh8UvpF9au0k7Dy5VGBk+3p9K9A0rwxdXwNxHFC6DG4lsYHtXLSF2uVPlIQg4kYAsPoe1XIbyaE7kncY9GNc1WdKTi1FrT3rPd99tC4yXU3dXMWibpGtFmlkOwP/e460VHpz3+qsSssRjiGWMx5/Ad6KxWG5veUNH3Zo3J6xPkO8tFm0mzsdx2wrtLqfvHOT+tdJpdza6VpxWVfMmYBUBP3aj8P6Bdz6ZYKltNLOybuR0ye9bN94Q1W2mBubNwpHytjIr9hXIrRbPmK1RuUuybOfmmedi5GSeSTVW4vWsNhWPzbhvuRL39z6Cuhe00zRoPO1B5JLmTiG3TkuayrbQ/HXiO7a3GgAYOIyqBWVfQmtXKL22Fh6XOud7fmeu/DfxlYa3bRaNqN7Y6brdqdtvdSDBkiPVM9OD610Pirwr8TNBittW0TVrjULUyF5pLZBsK9iVXOa8svPhHqWkeHE1HULkWFypJlkJBVF9BjnNTeGviP4u8IaPNb+EtXu7+KEFpJLsfuUH+yprxfY4ec3ZX/r7j6KjWg4pVPdsdc3xE8IWfihG1u3aV2RUe5jBjIOOTsJ+XB/E12D/Gbw5psMSabqqXCjIjMo3OARwP/wBdfJus61q/irxBe61q0iz3U775JAAoJ+grKaRFO0LyOuDXMshhB3pzcX5GlSvCMuWKPqw61qGuOLhbhbmSc7grSAMR7CvMfFXjnV9M16bSobQWbQvjMq5Z/f6VyfhnxDHNDDpF7O0ZjcPa3Ct88TDt7ivWx4Zl8daHFe3gjvNRsZ8RzRrtLoex9cV48cvhgq/76PNF39fJ+fn2OfkT1PNbjxVqen6tBLpd35ctt+8k3AMrynknaeK9E8EfEaTxB4hs7LVdNUXzMR9otxtV1xk7l/rTdJ8MazYaw1jB4fgAJ+aa6iBLep5pNcuLD4dsx06xjuPEF3yX24SBCecVpjIYWtD6vGneq1o9mvNvt6k0qkpystj1q5vCFItzsbsc4xWvoPi6707ZFfN9pgHBH8WPrXhfiD4r2ei2ccEa/b9QZBnGAgOOua4+w+L+tXGoq13BAlsoLPHGOSB2BrwsDlOYUl7eiuW3W+50KU4ytFnqXizw/PrHiSbWUv1QM27Dnnr61gz3+k6exXUdbt02DB2fO/5DpXe6JpGieKtAs9et1e9t7lcvDJKQVPcYBqHXPgNoc+j3V34ZlfT79kLiKR96OeuOeRXbg4/WZeyxU2mvJfn/AMA5JUkm21dniuufFTRtPLWug6dLeTDj7RdDav4L1NcRdeMtf1OXzZ754+fuR/Io/KvTvDvhi4k1waNr2iGR848wKMD8a6nWfgL4f1G3eSwu5LG7xkKMFCa+toYfBYbRR17vX+vkZxqQ2tY+drrWNRhnEX2hGlZcFHXPBqJJZFiyowp6lema6bxN4M1vwNdCPUNISRHbi5B3Bh7H+ldBp3gbXrrQxrH9kzR2hUOpcAEj1x6V7kIU1FNNK5GKq8quldHnIiubhdwVlXsSOtK1tPEmQS3qOtdjPYbCVOB+NR2UWhyTbJ9WjEoOAg6E+5rWUIRWp58MW5v3UcmLybT47XEW8NMJ2jYZD7TgAj06/nWgBHdO8ttF1w4QdMHoKu63f6THd/Zv3U8luCFeLnA69e9RP4ktLG2tRBp0UxuFJ84sQRtJGMD/ADzXHKnezS3/AK/I9JOU0lyk7WICF3YKUX5AvG5sfpUEd9qtxDJA6JFKsflKdvYsDz60yfx8YobWa20C0N0oO9pcujMDwdh46Y4rL0/VLptVkv5lV1jXzWjPCsc9MelE8P7ty6VKpF3lY1rzTb2xtTqsUZZgu1WUZBb610fgjRPFniJGb+z/ADIBwJXUjP0NUL/xlqeq6VHZOsEVsDuMaKAq/SvZfhZ8WdKntrfRNZiisXhASKdQFV/qO1ZLmUNtTHl5o8sjmo/gxctcG5fUXtrzduGY8gH1BzXOa34E8Q+Hr5JLkF1lbdHdRE4Y/Xsa+yIns7lFlkSNxjKsMc1yfizQpb/wlqNuo2O4MkK5BIKnI+lKFZ311M50Zcvus+bP7O8UtpssDK0kEgxIw5IHfJ64xVzwh4Aln1aK+i1G3l8olltkk++ccA+2cV60uh3mh+H7JInH2y8iYtuORjHTHrzWfonwkktIf7Y+0z2zwfvdhkHz45I4rCrTpyhKK92/YmlOrGST1PEvEWseOLbxesevrcW9yF+zCGWLEZiPG0DoVPrXWWWk3E+praJY38thcKI3mSEtCuPuMD2x6mvbNQ01fFd9ZXt3aQPbWgCQmYAk+pFX7rxH4a8KLL9r1WO18hNzQ7gcj0C1xTw9JxUYLl0tovufyPSlW1UUrnhkuieMbS/m0qSxysz4SeQ7EI7YPQ/SvRNGh17R4EuNVtJAhUAzAZUY6HI6fjT9C+OXhG5jns7+3lNm8pMbSqCQO2RXouk+ItA1e3e70TUIprV+JYcjKk98VlCUqcLTtHvpYxng7S5ovQpx69Z3Gn8TxyXGDhBjvXmviTwdoF5rVte2SPa+aC00CHgn29K63XdMt/7VNxb2ixwfxPBjlvUDtWDrcz6Jp/2tM3EStgI2MqT7149XMsTUvSorV6Jr8zKpCLXv9CtZ6Zp+m2pjs7QRsRzuPJ/GrAuhbYeKPDY5z61w114m1zUZQIY0hTPAU1jaro3jG4gN9a308yNy0QbGPpXJDJMTWd8RK1+7uSq8IqyPqTR73TdY8Hi1nW2lHlkSwhwSOOfcH3r5g+KWsReC7+3tfBniPVrSQyFhBK2+JB3Ksec5xxU/hKy1iOznuJpZvtwOFhkk2hh9a8x+KR1CTU7JL2zksmiVv3bnI5Ocg9819Zl9KUJRoStZafcux1Qmq1mbV38XPEOrW2dUgsLjU1QLHqfkbZlwR1AO1/T5lPWuWs/Ems2XiO51C11a6spL5z9p+xt5IfPP3VwOvauNYyzZjRvnYgLtNd/B8PdYhNiWhe4llCnYn8Lehr2JUqVJWfU9Ohhq1Z3i72PQNO8cy6omhSeNNMj11tHuPOuJo1AuLlAuESVj/rAoG7H0Br1O4+Mvha0jjm0HwzeXAkUvlNsQRj/eXoB9K5Pw/wDBzUr6RtX8QgxSTtujjt5whiXGPm9SR6dK6nUfhJplvbItrbux2AFA42/mTXn1GtE2Z4qqoPkir26nlPxH8bT+KrjSrMWyRQW6l2xHy0hPJz16YH1rrPBvxXutMt0sPEMUctoke5JApD56Yx3NR6tpXh3RJJ764e3N3ECxYyh23Y6Z/oK8VvtdE9y889yWkJJA9PpQoSqJKPQ8xzi5OUIu59Fal8VPB15rFxcK95G1woVTs24wBt759cjvWpqfxDWa3g0/TtghiYGRBJhl3DglfT+Wa+V9Fu3uNehZ13iPdIcn0Ga1Lb7bYawdYhv1uJpBlwOnP8JrmxGAqTk4wm1pt3/Uqc+SN56XPQtY+IPig339nLci3jV9iqBuBGayL7X72XU20+0vJo1iOCFOASetdBpFra+I5LfWIbYBY4y9wmchHX/GrY+G93DeXGtRyo1m0ZuS7nB9cYrz6FKjdJws0vxuZOTSbepyWg/2+mvwGHUZoQZBvdnLDGeeO9fUfgyDw3c6XejWb5GJbKM7hdmB6187WNtdJMNQjgAhRsqGP3j6mszWrnW7bXHtFlkRLsCRUVuCDWmMwHta0ZxjFpb3W/8Aww8NiYqL5ke1+NfhjPdyyax4a1+K9ifaYoGl2tn0B6VR8N6P4k0qdrXX28lGBLRsdx2+xFZ3w+tL/R5IbzWriS5tmIURK3EZP8R+lfQCJp16VCmKR4wOrAkj1NeJUjTxEXQoySadl1+653xoxa9pG6Pnvxp8CdQnuU1Xwwvn2dwnmPG3DI3XA9a8flsLvw/fPZmG4tbtc7kkQq1fc15NqQjkgsmjVCMLuPSvPPEuoJNqSwX2mwSSwLhZioLMe/Jr2quYrB4blqJytpr1+ZzVsNTb5oux5h8Nde8Uaoz2twwjtLZCUuJQQd3Ye9VPEms6Jd+J7W18YaRJcRF96XVqWCBc4IYenfivRbS7s2k+QrCwPTgYqXUtTfTVWSR4Zo2HHmRq34c189hs0wsa7n7Dlb2aev8AkbJ/u+Vu5w1l4L0uy1XdZ2iXFjdfcMrBgV6jFb82mWOn+WYoliVQUAQBVGf/ANVdBpWu6V4ogMV3aRQRWfKyQgIB+Vea+N/iZovhy8l0y2s5b0P92QnIFXXw+Mx1XmpXkvu/U5oUFC7jrc6t76ONwpHJAHynk/hWJq+vtZTNBABPO5yER8PXEWXjnWtcZZ9J0ppQTtJ2cg+ldPYaVr18X1O78OyoUUkSEIsjfQZ5pwyTEU3epC/kmZyn0W5yniu8w9vcRNMLqaPM27GQfQEdq4m41a3tpEimcrLKdq5BIzXWa2NS1LVhHZ6TqBjCciaDbtbuMjr9axX8Laxbp9pubCYRlucpk19hhYRoUVCejOCVO9S819xoeG9fubDxBaQNGVErAJMnHPcGu5sde8QeJtBTTfC1imkywXZ86/U7ANpPK9y2eteaTapHommR3p0uVZjIUtzN3I6t7YqfRfiR4itZmu/tEKWkfWFEHzk9gPX3rysXgKlR+2pQXMrWu+vp1366Hp0YtLXQ+jNI0/8As+1Bmme8v3GZ7uY5kmbuSew9quSF8bhkDp64r5tv/jF4ymy9s1rbIDkARbj+ZqceJPiT8Sru6i0Jriy01Nu6FJAAvHIL8E5OTXz64axtWbqVZxV93e/6HQ6kYq9z2rWfF+h6JA39q6lDGMcREiRm9tgr548W+L7DV/Et3f2FqtrasFSGIIEwAMdBwMnJ/GuntPgP4/vLeWcR2kkmMgSXGGc+mTXF+Ifhl428NXKz6zoNzBCT/rFAkT/vpcivrsoyejgG5Rm3J/Jfcc83GqrS2MuLWbh9wFj5jDod3H41be+lYWkrwmN45drqDwwOP0606yhiV/KTavPIPX8a7LS/Cb63YyWcklqts0qyLIx2uCvYH8uOlfR1KSnG76HnwqU1O3LY5XUNTt7GdbcSs8hbBXpimwvrD2rXc+mXdvAAWMoGVC1RtQ2oeJg1vCZ7l3IVY08zJ6YA7V69Z/DzxhLErXVl9hiVOWupAsZDcbfqc9K4qnJThq7HWoqm1FRuebeGvGfjPS/Egm8LwXU6qQmEi3b19H6jn0NfRWh/FDRrq3Fr4+0a40V7gNHIFHm205HyNnGSuOhHOK+e5b/wx4R0m/ttJ1O9n8QTu9vKsTFILXa2OCeHPB5HTsa5eSa4ttN824uWWWYgxQsx3bTyWx2zx9a5eWU5e4uWO2qd35/0j03CMVsfa0a+HLOCwNjqdlqWjsPKk85hI0EQ5TJI3AY3Lu4/h+taI0LQ9Q0ORdOmttXi3F0tppBcJGe21j8y5HHXGce9fGHhvxFrkvhrX9LtZpDttg6Ddyq7sOB+BqPwb4gv9G1Ka/tZilxbBZVGflOGGeOh4NZU6dWk6l7OzWnql9wOMHa59WX8ui+EtOi1zS9VfTbePJltfMPlSqR0VWJ2SA9hwa8O8SeMI/EuriTQtOh0+2R8tC8Q2zykf6xl+6CemP51bv8ASW8YzJJoPiqC7d0aU6dfPtkR25bbkYOT+X0rLtfAHjmKGRYPCt55ncphhx+PNJ1FWjyx37f8A5pPl0S0OLl1zUJNTfzWFszTAy2u3bECvUeX05/xr0G++I+t+IhGNUsNId1I2TG12CMDsdvJHtisfVfDPie0ha91bwveRWsZBM00OCpPGCepX2NdJ4Z+DF3r+gpq8XiRNNecbooWiZ1x/tnIx+Rqq+NoYWCeIfItv6sW6Uqkbo0NGgnuI7i8j8ZxWk7xsgS1tmxsxk4AOVHueR1zUnh6TRdJmeO48Y2aSycyybi8ajqMA4EjA+pwD2Nbo+HXirT/AIbXmif2fBd3k6sWa0dMttYbfm4JyM8GvnPWTd6Vdy2UySQXcLFJI5DgofQj1qMPXhjJtU+j+9d15D+qTVm9T0zxN4hjsr/+z9EvLiHTY5VuIyzq2+bHMoK8AnPIrhBFe6x4h1XVru2RIolMpigTZECxwuAOgzzUOnHVb3QvMmtt0QOVYjk47imWfiLWNPleDT5SqyDbJblQ6yD0IPWux8/K4QtdeZko2k0aiaazbXkn3EAZYjr+FW0htYyGZSTjCgjOK6LVvAXjfRNHs/EGqaN/Z9lcxCRlklXEeegIJyCR261xz63aiZomhOOm4Nwa3jOEvdSPPqUqyep0GhK8viKCeyly5yoQngjBz+lc/d3N3b3BVWMsTZIKc4NVv7YmhmWSy2wsrY9/wNej2/w98R33g+712ys45YYkE3mRuMlcZbC9TjvWPsoxqOa6pfr/AJmijJRtIwtHu55NBe41UFIrc5CpgNKx+6nPrzk+grmrU3Ms7yjzPtMrbi2Tlfoa2rG3kOgF70iVlkaSM5yDgY5/OsePUriKRDhg5GSy1zYdKVWaiup01HLkXKbElheXVz595OC7ADfK3zNgYFdpovw/8QvYi+s1t70ld0SWt0u8/ga5bwnqGhSa8jeJpZGjB+6vRvrX1H4Ks/CU1sZPD9xDINg2rkFoiO+eooxM5UlyxWpzwozm7zPKf7T8Q6bo7WHi7Rr62018KJBlZVbP977pHsa5u7j8OeINTNppusxWjH/VpeowyfQuMjNfVVnp9ld27RzrFPtY70Y5Ga5/xJ4Js7hY7mysrMLE4Z1eFSWTuAw5B9DXm0qmKhacop37Pb5W/U65UIuNmz53u/hL4wQKbC3truOTB3xTLj8zWVL8P/GGhXLS3ukSpGo3M8YEqAe+3PFe0v4T1zR9xsdRZrcMdiSnOF7YOf51a0GxmuZbiSe4YyHGQzdfpXnV89r4eTp1Kab9WvzFHDwtpc810fwlrWqQq8moabYxSLkFpMOvttOMfnW7H4Kn8M6edTvHGoyqcqbdjJGPduOKzviVZa1ZXMd5oF7P5cSEzKr4IbPp3rzrS/iFrEeoQJeX1wu1sFkJQ/jtxn8a2hVxGY0OaFknuk9V5GsaVJPbU9Dm+K2r2V2IJLKC7jGF2S84HtivSdH8RaX4n0uG4vdAigdPvSSMPlPrknIFedR+I/DniW8NjqOlWthqDqDHqUEQXzj6kDFcP41uNe8JXwtWuY5oLhN0Utu5KOv88+1Yf2XCpL2dGKhJ7t63X9fMOaalZ6o2dd1/xZZX9zarfeVGWYIVVWUjPG1sfrXmE7v5zsqliSSTjvXqfhT+0NX8NK+o6X5kkh/czbMnPY4710HhD4Q3N7dSXmvqkcW8lXUgZH+6ele/hqVHDJxUUvRbnNzNys3c8ALuT80jbfrXZfDz+1Rr5j8O30dpfshIaabytw7gHofpX0Onw68MWKSSQ6dBcMFIzNgg/gwxXgXj5tG0vVpo9HWC0n2gstvkAHJyPauhzjWThbc6qF9dD6YsL2fTtFsrzxZe6eb2MBWml24564ZeCPrWh/wsTwzaXKWWr7bWNx+5vLfBicfUdK+GF1rUAxZ7uRkHABYkVLJrmoy2mwzFUB5IPB/CsVl8qcuai+W/Tp9x0JySPrfXtDttQsmNnrqajbiRntIYW2YUjOC3bnt0rz2w0PxHYasWfxNYu8EoY2sD7mT2DEY/I15d4W8baxoE8dzY3fnJGdzwSLlQPUVe+IXi3V77ULXVIPKsI7qIN/oo2hsdz71588vnKo1ZXl31WnrszNrW/U+ktU1a0u1tGu75GPl4O1QrR89D6msVpbSO68oqt5D99S3JHt/+qvmGDxxrAsw011JdGNsfM3IFXP8AhO9Vuo1jGoOgxgLu2/hmvLrcNYidV1VJa+v66hLnvse0+I18M2VpNdyacgPXk4Zz7H+leRzX0kupebbxpHGWyExwB6Vmvd6xfSokkhOSB+8fI/Wuk1TwhqtnpBuIXjefZuKxuGx9K9rBYB4SNqk+Zv7jjqOKaI7m/hyShUv3QHkVnSzDOVOQe9c/ZTR2t2rXrHecqyt1q3YwSSaqYUcMGyclsLivUhhY0loypU7bGstw8bKUBJNaUmqOIY2iyt1GxD45G3tWS+padbXIXPnOjDIXgfnWfe3k97qjzWaCFJG4QHpUyoKTWgoQfVWOqi166eSTzEjcy4BO3kfT0r0Dw5oV1qVvDcxzxKrdTJJtA+tcb4M8G+ItbS4v4dNF3DbDgMwXe3pWxqupeJ/CcCwarALNLgF0Ee0qMdV4715OJo88vZ0Un31saRp33Pb/AA7o+vaXKXtjbXkDY3RwuWOfUH1rR1z/AE+yks4pUsZznfHNBk59fUfUA18u/wDCxfEEUbDT9Qnt938avjFbVl8cvGKWwtrl7XV9nAa7hDOB/vDBrR5ZOVF00reTd/8AJnTDljpc7i48N6s8KTW8cdyiSNFMEkGFzzuz3FV7DStF0mVpL0XM8sh+9vKqh/2ax7H4vi7WdL3QIYrtlyk9u+0AehXofrWZP8VtR8Ryw6JDaRRWySj5Ao3N25NeY8uxUJOFPRJXbXV9tRyjFK6Z7Po/iOO3Xzl1MiCPlIXmVWc/7THnH0rZ1PW/tdrCGvLJFkyweLkMf7uTzmvkHxUdU0nUpIJ96bnO1t3DD1BrFi1bUGQR/b5goOdokOBXZUyipicPySqKz6pa/mCk4n1Fq97p+lpueUI8h24xyPeuY1G60x3tJba7Y3KyD514wD15rH8GzahrvgrUDczrci1Q7WcZZTjsTXoui/D+y1bwosWpyAmRBIHQ4ZCRXk0ckWHfvTd1+JyVKjvoY9zrumW0I+1XUEaqMA55NYs/j/QID5cLPJz1YcVLc/C+8ilitVi+227OVWXcPl+tWLn4J4QPFclT324bH4GtaWS4SP8AEm3cxVZ3tZnKal4+l85TYWrGMdSF+9VEeMfEcyFraxAA55FdBefDLxbosbXthPFfW8fzbBgNj6GiGZJEQ3WkBZu/O3BHrXprLsLTiuSCkhub3Rg/8JB4v1GJYbezljkByWjUiivQbDxFdRwgmxSFAdvmJg4+oorNYfl0hSjYrmb3P//Z"), linear-gradient(180deg, #6fb8ea 0%, #a9d8f2 34%, #eaf6e2 60%, #8fc96f 78%, #4e9e4a 100%);
    background-size: cover; background-position: center;
  }
  .brand .logo .mark::after {
    content: ""; position: absolute; left: 50%; top: 50%;
    width: 10px; height: 10px; border: 2px solid #fff; border-radius: 50%;
    transform: translate(-50%, -50%);
  }
  .brand .sub { margin-top: 4px; font-size: 12px; color: var(--text-sub); }
  /* æŸ¥è¯¢æ  */
  .query-bar { display: flex; gap: 8px; margin-bottom: 14px; }
  .wrap { max-width: 480px; margin: 0 auto; padding: 16px 16px 28px; }
  /* ========== æŸ¥è¯¢æ  ========== */
  .query-bar { display: flex; gap: 10px; }
  .query-bar input {
    flex: 1; height: 42px; padding: 0 12px;
    border: 1px solid var(--border); border-radius: 8px;
    font-size: 14px; background: #fff; outline: none;
    flex: 1; min-width: 0; height: 46px; padding: 0 14px;
    border: 1px solid var(--line); border-radius: var(--radius); font-size: 15px;
    background: #fff; outline: none; color: var(--text-strong);
    transition: border-color .2s, box-shadow .2s;
  }
  .query-bar input:focus { border-color: var(--primary); }
  .query-bar input::placeholder { color: var(--text-mute); }
  .query-bar input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(31, 122, 56, .1); }
  .query-bar button {
    height: 42px; padding: 0 18px;
    border: none; border-radius: 8px;
    background: var(--primary); color: #fff; font-size: 14px;
    cursor: pointer;
    flex: 0 0 auto; height: 46px; padding: 0 24px;
    border: none; border-radius: var(--radius); font-size: 15px; font-weight: 600;
    letter-spacing: 1px; color: #fff; cursor: pointer;
    background: linear-gradient(180deg, var(--primary-light), var(--primary));
    box-shadow: 0 2px 6px rgba(31, 122, 56, .25);
    transition: opacity .2s;
  }
  .query-bar button:active { background: var(--primary-light); }
  .query-bar button:active { opacity: .85; }
  /* çŠ¶æ€ */
  .state { padding-top: 8px; }
  .loading { text-align: center; padding: 48px 0; color: var(--text-sub); }
  /* ========== çŠ¶æ€æç¤º ========== */
  .loading { text-align: center; padding: 48px 0; color: var(--text-sub); font-size: 15px; }
  .spinner {
    width: 32px; height: 32px; margin: 0 auto 12px;
    width: 32px; height: 32px; margin: 0 auto 14px;
    border: 3px solid var(--primary-bg); border-top-color: var(--primary);
    border-radius: 50%; animation: spin .8s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
  .empty-tip {
    padding: 12px 16px; border-radius: 10px; background: #fff;
    box-shadow: var(--card-shadow); font-size: 14px; color: var(--text-sub);
    text-align: center;
    margin-top: 16px; padding: 16px 18px; border-radius: var(--radius);
    background: #fff; border: 1px solid var(--line); box-shadow: var(--shadow);
    font-size: 15px; line-height: 1.7; color: var(--text-sub); text-align: center;
  }
  .empty-tip.error { color: var(--red); background: #fdecec; }
  .empty-tip.primary { color: var(--primary); background: var(--primary-bg); }
  .empty-tip.error { color: var(--red); background: #fdf3f3; border-color: #f3dcdc; }
  .empty-tip.primary { color: var(--primary); background: var(--primary-bg); border-color: #d7ead9; }
  /* å¡ç‰‡ */
  .card {
    background: #fff; border-radius: 12px; box-shadow: var(--card-shadow);
    margin-bottom: 12px; overflow: hidden;
  /* ========== åŒºå—(参考页:绿竖条标题 + ç»†çº¿ + ç™½åº•描边卡片) ========== */
  .sec { margin-top: 22px; }
  .sec-hd {
    position: relative;
    display: flex; align-items: center; gap: 9px;
    padding-bottom: 11px;
    border-bottom: 1px solid var(--line);
    font-size: 18px; font-weight: 700; letter-spacing: .3px;
    color: var(--text);
  }
  .card .card-hd {
    display: flex; align-items: center; gap: 8px;
    padding: 12px 16px; border-bottom: 1px solid var(--border);
    font-size: 15px; font-weight: 600;
  .sec-hd::after {
    content: ""; position: absolute; left: 0; bottom: -1px;
    width: 46px; height: 2px; background: var(--primary);
  }
  .card .card-hd .bar { width: 4px; height: 16px; border-radius: 2px; background: var(--primary); }
  .card .card-hd .count { margin-left: auto; font-size: 12px; font-weight: 400; color: var(--text-sub); }
  .card .card-bd { padding: 12px 16px; }
  .sec-hd .bar { width: 4px; height: 18px; border-radius: 2px; background: var(--primary); }
  .sec-hd .count { margin-left: auto; font-size: 12px; font-weight: 400; color: var(--text-sub); }
  .sec-hd .caret { font-size: 12px; color: var(--text-mute); transition: transform .2s; }
  /* ä¿¡æ¯è¡Œ */
  .kv { display: flex; padding: 6px 0; font-size: 14px; }
  .kv .k { flex: 0 0 88px; color: var(--text-sub); }
  .kv .v { flex: 1; word-break: break-all; }
  .kv .v.dim { color: var(--text-sub); }
  .sec.collapsible .sec-hd { cursor: pointer; }
  .sec.collapsible:not(.open) .sec-hd .caret { transform: rotate(-90deg); }
  .sec.collapsible .sec-bd { display: none; }
  .sec.collapsible.open .sec-bd { display: block; }
  /* å¾½ç«  */
  .badge {
    display: inline-block; padding: 2px 8px; border-radius: 4px;
    font-size: 12px; line-height: 18px;
  .panel {
    margin-top: 12px; padding: 15px 16px;
    background: #fff; border: 1px solid var(--line);
    border-radius: var(--radius); box-shadow: var(--shadow);
  }
  .badge.green { color: var(--green); background: #e8f5e9; }
  /* ========== å­—段行:标签小一号浅色,值深色加粗,形成层级 ========== */
  .kv { display: flex; padding: 6px 0; font-size: 15px; line-height: 1.65; }
  .kv .k { flex: 0 0 auto; padding-top: 1px; font-size: 13px; color: var(--text-sub); }
  .kv .k::after { content: ":"; }
  .kv .v { flex: 1; min-width: 0; word-break: break-all; color: var(--text-strong); font-weight: 500; }
  .kv .v.dim { color: var(--text-sub); font-weight: 400; }
  .kv .v .link { color: var(--primary); text-decoration: none; border-bottom: 1px solid rgba(31,122,56,.35); }
  .tip { margin-top: 8px; font-size: 15px; line-height: 1.75; color: #374151; }
  .meta {
    margin-top: 10px; padding-top: 10px;
    border-top: 1px dashed var(--line);
    font-size: 12px; color: var(--text-sub);
  }
  /* ========== é˜²ä¼ªæŸ¥è¯¢ï¼šçŠ¶æ€æ¡ ========== */
  .verify { display: flex; gap: 10px; padding: 13px 14px; border-radius: var(--radius-sm); }
  .verify.ok { background: linear-gradient(135deg, #eff8f1, #e2f2e6); border: 1px solid #cfe7d6; }
  .verify.warn { background: linear-gradient(135deg, #fff9ec, #fdf1d6); border: 1px solid #eeddb4; }
  .verify .vico {
    flex: 0 0 22px; height: 22px; margin-top: 1px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%; color: #fff;
  }
  .verify.ok .vico { background: var(--primary); }
  .verify.warn .vico { background: #d2961b; }
  .verify .vico svg { width: 13px; height: 13px; }
  .verify .vtxt { flex: 1; min-width: 0; }
  .verify .vt { font-size: 16px; font-weight: 700; line-height: 1.4; color: #1b4332; }
  .verify.warn .vt { color: #8a5a00; }
  .verify .vs { margin-top: 4px; font-size: 13px; line-height: 1.65; color: #5b6670; }
  /* ========== é˜²ä¼ªç¼–码 ========== */
  .code-box {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    margin-top: 12px; padding: 10px 12px;
    background: #f8faf8; border: 1px dashed #cfe0d2; border-radius: var(--radius-sm);
  }
  .code-box .cl { flex: 0 0 auto; font-size: 13px; color: var(--text-sub); }
  .code-box .cv {
    font-family: var(--mono); font-size: 16px; font-weight: 700;
    letter-spacing: .5px; color: #14532d; word-break: break-all; text-align: right;
  }
  /* ========== æŸ¥è¯¢è®°å½•(统计格) ========== */
  .stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
  .stat { padding: 9px 11px; background: #fafbfa; border: 1px solid var(--line); border-radius: var(--radius-sm); }
  .stat .sl { font-size: 12px; color: var(--text-sub); }
  .stat .sv {
    margin-top: 3px; font-size: 13px; font-weight: 600; color: var(--text-strong);
    font-variant-numeric: tabular-nums;
  }
  .stat.span2 { grid-column: 1 / -1; }
  .stat.count { background: var(--primary-bg); border-color: #d7ead9; }
  .stat.count .sv { font-size: 21px; font-weight: 800; color: var(--primary); letter-spacing: .5px; }
  .stat.count .sv .u { margin-left: 3px; font-size: 13px; font-weight: 600; }
  /* ========== åº•部说明 ========== */
  .notice {
    margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--line);
    font-size: 12px; line-height: 1.75; color: var(--text-mute);
  }
  /* ========== å¾½ç«  ========== */
  .badge { display: inline-block; padding: 3px 9px; border-radius: 6px; font-size: 12px; line-height: 18px; }
  .badge.green { color: var(--green); background: var(--primary-bg); }
  .badge.red { color: var(--red); background: #fdecec; }
  .badge.blue { color: var(--blue); background: #e3f2fd; }
  .badge.gray { color: var(--gray); background: #f1f2f4; }
  .badge-big { display: inline-block; padding: 4px 12px; border-radius: 16px; font-size: 14px; }
  .badge-big.green { color: #fff; background: var(--green); }
  .badge-big.red { color: #fff; background: var(--red); }
  .badge-big.gray { color: #fff; background: var(--gray); }
  /* ========== æ˜Žç»†æ¡ç›® ========== */
  .li { padding: 10px 0; border-bottom: 1px dashed var(--line); }
  .li:first-child { padding-top: 0; }
  .li:last-child { padding-bottom: 0; border-bottom: none; }
  .li-hd { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 2px; }
  .li-ttl { font-size: 14px; font-weight: 600; }
  .li-time { margin-left: auto; font-size: 12px; color: var(--text-sub); }
  .li-bd .kv { font-size: 13px; padding: 2px 0; }
  .li-bd .kv .k { flex: 0 0 auto; min-width: 74px; }
  /* åˆ—表 */
  .scroll-list { max-height: 320px; overflow-y: auto; }
  .scroll-list::-webkit-scrollbar { width: 4px; }
  .scroll-list::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 2px; }
  .tbl-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .mini-table { width: 100%; border-collapse: collapse; font-size: 13px; }
  .mini-table th, .mini-table td { text-align: left; padding: 8px 6px; border-bottom: 1px solid var(--border); }
  .mini-table th, .mini-table td { text-align: left; padding: 7px 6px; border-bottom: 1px solid var(--line); }
  .mini-table th { color: var(--text-sub); font-weight: 400; background: #fafbf9; white-space: nowrap; }
  .mini-table tr:last-child td { border-bottom: none; }
  .mini-table td { word-break: break-all; }
  .mini-table td { overflow-wrap: break-word; }
  .pill-list { display: flex; flex-wrap: wrap; gap: 8px; }
  .pill {
@@ -135,55 +227,58 @@
    padding: 6px 12px; border-radius: 16px; font-size: 13px;
    background: #f3f4f6;
  }
  .pill .dot { width: 6px; height: 6px; border-radius: 50%; }
  .scroll-list { max-height: 300px; overflow-y: auto; }
  .scroll-list::-webkit-scrollbar { width: 4px; }
  .scroll-list::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 2px; }
  /* æ­£å“éªŒè¯å¡ï¼ˆé¡¶éƒ¨ï¼Œæ‰«ç ä¼˜å…ˆå±•示) */
  .verify-card {
    border: 1px solid var(--primary-light);
    background: linear-gradient(180deg, #eef8ee, #fff);
  /* ========== å…¬å¸ä»‹ç» ========== */
  .company-stats { margin-bottom: 12px; }
  .company-stats .pill { background: var(--primary-bg); color: var(--primary); font-weight: 600; }
  .company-bd { font-size: 16px; color: #374151; line-height: 1.9; text-align: justify; }
  .company-bd p { text-indent: 2em; }
  .company-bd p + p { margin-top: 10px; }
  .company-clip { position: relative; max-height: 164px; overflow: hidden; }
  .company-clip::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 56px;
    background: linear-gradient(180deg, rgba(255,255,255,0), #fff);
    pointer-events: none;
  }
  .verify-card .card-bd { padding: 14px 16px; }
  .verify-title {
    display: flex; align-items: center; gap: 8px;
    font-size: 16px; font-weight: 700; color: var(--primary);
    margin-bottom: 4px;
  }
  .verify-title::before { content: ""; width: 5px; height: 16px; border-radius: 2px; background: var(--primary); }
  .verify-sub { font-size: 12px; color: var(--text-sub); margin-bottom: 10px; }
  .verify-badge { margin-bottom: 10px; }
  .verify-stats { margin-top: 10px; border-top: 1px dashed var(--border); padding-top: 4px; }
  .company-clip.open { max-height: none; }
  .company-clip.open::after { display: none; }
  .company-more { margin-top: 12px; text-align: center; }
  .company-more a { color: var(--brand-blue); font-size: 15px; cursor: pointer; }
  /* å¯æŠ˜å å¡ç‰‡ */
  .card.collapsible .card-hd { cursor: pointer; position: relative; padding-right: 30px; }
  .card.collapsible .card-hd::after {
    content: "▾"; position: absolute; right: 12px; top: 50%;
    transform: translateY(-50%); color: var(--text-sub); font-size: 12px;
    transition: transform .2s;
  /* ========== è”系我们 ========== */
  .contact-item { display: flex; gap: 12px; padding: 13px 0; }
  .contact-item:first-child { padding-top: 0; }
  .contact-item:last-child { padding-bottom: 0; }
  .contact-item + .contact-item { border-top: 1px dashed var(--line); }
  .contact-item .ico {
    flex: 0 0 30px; height: 30px;
    display: flex; align-items: center; justify-content: center;
    border: 1px solid var(--primary); border-radius: 50%;
    color: var(--primary);
  }
  .card.collapsible:not(.open) .card-hd::after { transform: translateY(-50%) rotate(-90deg); }
  .card.collapsible .card-hd .count { margin-right: 16px; }
  .card.collapsible .card-bd { display: none; }
  .card.collapsible.open .card-bd { display: block; }
  .contact-item .ico svg { width: 15px; height: 15px; }
  .contact-item .txt { flex: 1; min-width: 0; }
  .contact-item .lbl { font-size: 14px; font-weight: 700; letter-spacing: .3px; color: var(--primary); }
  .contact-item .val { margin-top: 2px; font-size: 15px; line-height: 1.6; color: var(--text); word-break: break-all; }
  .contact-item .val a { color: inherit; text-decoration: none; }
  footer {
    max-width: 480px; margin: 0 auto;
    padding: 20px 16px 28px; text-align: center;
    font-size: 12px; color: #9ca3af;
    padding: 20px 16px 32px; text-align: center;
    font-size: 12px; line-height: 1.8; color: var(--text-mute);
  }
</style>
</head>
<body>
<div class="brand">
  <div class="logo"><span class="mark"></span>种子产品溯源</div>
  <div class="sub">来源可查 Â· åŽ»å‘å¯è¿½ Â· è´£ä»»å¯ç©¶</div>
</div>
<header class="hero">
  <div class="hero-top">
    <img class="hero-logo" src="data:image/png;base64,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" alt="塔河种业">
    <div class="hero-name">新疆塔里木河种业股份有限公司</div>
  </div>
  <div class="hero-banner"></div>
</header>
<div class="wrap">
<main class="wrap">
  <!-- æ‰‹åŠ¨æŸ¥è¯¢æ ï¼šæ—  code å‚数时提示输入;有参数时自动查询 -->
  <div class="query-bar">
    <input id="codeInput" type="text" placeholder="请输入追溯码(袋码 / æ‰˜ç›˜ç  / æ‰¹æ¬¡ç ï¼‰" autocomplete="off">
@@ -191,22 +286,101 @@
  </div>
  <section id="state" class="state"></section>
</div>
<footer>本系统数据仅用于产品溯源,请通过正规渠道购买。</footer>
  <!-- å…¬å¸ä»‹ç»ï¼ˆå¸¸é©»ï¼‰ -->
  <section class="sec">
    <div class="sec-hd">
      <span class="bar"></span><span class="ttl">公司介绍</span>
      <span class="count">新疆 Â· å›½æœ‰æŽ§è‚¡</span>
    </div>
    <div class="sec-bd">
      <div class="panel">
        <div class="pill-list company-stats">
          <span class="pill">成立 2002 å¹´</span>
          <span class="pill">注册资本 1.1 äº¿å…ƒ</span>
          <span class="pill">自主品种 61 ä¸ª</span>
          <span class="pill">研发团队 16 äºº</span>
        </div>
        <div class="company-bd company-clip" id="companyClip">
          <p>新疆塔里木河种业股份有限公司于2002å¹´12月30日在新疆维吾尔自治区工商局注册成立,是一家集农作物种子(棉花、小麦、水稻等)育、繁、推、加、销、科服为一体的国有控股公司。公司起始注册资本6500万元,2015年增资后注册资本达1.1亿元,由新农开发、隆平高科、塔里木大学科技公司、巴楚丰和种业四家股东单位组成。旗下拥有两家控股子公司,分别是主营生产、加工、批发销售农作物种子包衣剂的塔河勤丰全资子公司,以及以农作物种子生产、经营及农业技术开发、推广、服务为主的伽师塔河种业公司。</p>
          <p>公司自成立以来,始终坚持以农业种子产业化经营为目标,全力打造“育种能力强、生产加工技术先进、市场营销网络健全、技术推广服务到位”,“育繁推”一体化、“产学研”相结合的现代农作物种子经营企业。公司拥有强有力的16人研发团队,坚持科研创新的原创性,坚持自育为主,同时通过建立联合生态试验站,柔性引智,常年外聘中科院院士、中国农大、浙大、中棉所等12名国内含博导在内的顶级棉花专家团队,与一师农科所开展了连续十五年的院企合作,解决产业前沿信息和优质种质资源“短板”问题。目前,公司拥有自有知识产权品种61个,其中棉花品种46个,粮食品种15个。凭借在种业领域的突出表现,公司相继荣获兵团和国家农业产业化重点龙头企业、高新技术企业、3A信用企业、新疆名牌、中国名牌;国家、自治区3A级标准化良好行为试点企业、中国种业信用骨干企业、自治区企业技术中心、新疆产学研联合开发示范基地、兵团首批创新型企业、全国农垦农作物良种展示示范基地等荣誉称号。自2020年起,公司棉花商品种子销售连续位居全国榜首,2022年入选国家农作物种业棉花补短板阵型企业,2023å¹´1月荣获新疆农作物种业优势企业,2025å¹´7月荣获兵团创新型和“专精特新”企业称号,2025å¹´8月“机采棉新品种选育及提质增效关键技术集成应用”获得兵团科技进步奖一等奖,2026年荣获全国棉花种子育繁推生产经营A证。</p>
        </div>
        <div class="company-more" id="companyMoreWrap"><a id="companyMore">点击查看更多</a></div>
      </div>
    </div>
  </section>
  <!-- è”系我们(常驻) -->
  <section class="sec">
    <div class="sec-hd">
      <span class="bar"></span><span class="ttl">联系我们</span>
    </div>
    <div class="sec-bd">
      <div class="panel">
        <div class="contact-item">
          <span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg></span>
          <span class="txt"><span class="lbl">公司地址</span><div class="val">新疆阿拉尔市南泥湾大道1865号</div></span>
        </div>
        <div class="contact-item">
          <span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1 1 .4 1.9.7 2.8a2 2 0 0 1-.5 2.1L8.1 9.9a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.4c.9.3 1.8.6 2.8.7a2 2 0 0 1 1.7 2z"/></svg></span>
          <span class="txt"><span class="lbl">联系电话</span><div class="val">行政管理部:<a href="tel:09974615802">0997-4615802</a></div></span>
        </div>
        <div class="contact-item">
          <span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5 12 3l9 6.5V20a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9.5 22v-6h5v6"/></svg></span>
          <span class="txt"><span class="lbl">公司网址</span><div class="val"><a href="https://www.talimu.net" target="_blank" rel="noopener">www.talimu.net</a></div></span>
        </div>
        <div class="contact-item">
          <span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.4 8.4 0 0 1 3.8-.9h.5a8.5 8.5 0 0 1 8 8z"/></svg></span>
          <span class="txt"><span class="lbl">微信公众号</span><div class="val">塔河种业之家</div></span>
        </div>
      </div>
    </div>
  </section>
</main>
<footer>
  æœ¬ç³»ç»Ÿæ•°æ®ä»…用于产品溯源,请通过正规渠道购买。<br>
  æ–°ç–†å¡”里木河种业股份有限公司
</footer>
<script>
(function () {
  'use strict';
  // ========== é¡µé¢é…ç½® ==========
  // ç”Ÿäº§ç»è¥è€…名称与追溯网址是种子标签四要素的组成部分,必须与二维码/标签印刷内容一致,
  // å› æ­¤ç»Ÿä¸€ç”±åŽç«¯ä¸‹å‘(/public/mes/trace/config),禁止在此硬编码。
  var COMPANY_NAME = '';
  var TRACE_URL = '';
  var IS_IMPORTED = false;
  // æ‹‰å–溯源页配置;失败时不阻断查询(查询结果本身也带生产经营者与追溯网址)
  function loadTraceConfig() {
    return fetch('/public/mes/trace/config')
      .then(function (resp) { return resp.json(); })
      .then(function (res) {
        if (res && res.code === 0 && res.data) {
          COMPANY_NAME = res.data.operatorName || '';
          TRACE_URL = res.data.traceUrl || '';
          IS_IMPORTED = !!res.data.imported;
        }
      })
      .catch(function () { /* é…ç½®æ‹‰å–失败:退回使用查询结果中的同名字段 */ });
  }
  var STATE = document.getElementById('state');
  var CODE_INPUT = document.getElementById('codeInput');
  var SEARCH_BTN = document.getElementById('searchBtn');
  var BATCH_STATUS = { 10: ['草稿', 'gray'], 20: ['生产中', 'blue'], 30: ['已完成', 'green'], 40: ['已作废', 'red'] };
  var BAG_STATUS = { 10: ['已生成', 'gray'], 20: ['已导出印刷', 'blue'], 30: ['已作废', 'red'] };
  var QUALITY_RESULT = { 0: ['处理中', 'gray'], 1: ['合格', 'green'], 2: ['不合格', 'red'] };
  var WORKER_TYPE = { 10: '组长', 20: '操作工', 30: '包装工', 40: '质检员', 50: '其他' };
  var FLOW_DIRECTION = { 1: ['入库', 'green'], 2: ['出库', 'blue'] };
  var FLOW_BIZ_TYPE = {
    110: '采购入库', 111: '采购入库取消', 112: '调拨出库', 113: '调拨入库',
    114: '其他出库', 115: '其他入库', 116: '生产领料', 117: '生产退料',
    118: '生产入库', 119: '销售出库', 120: '销售退货入库', 121: '外协发料',
    122: '外协入库', 123: '生产消耗', 124: '产品产出', 125: '供应商退货出库',
    126: '库存调整入库', 127: '库存调整出库'
  };
  var SOURCE_TYPE = { BAG: '袋码', PALLET: '托盘码', BATCH: '批次码' };
  // ========== å·¥å…·å‡½æ•° ==========
  function esc(v) {
@@ -225,8 +399,18 @@
    return esc(String(s).replace('T', ' ').replace(/\.\d+$/, '').slice(0, 16));
  }
  function fmtDay(s) {
    if (!s) return '-';
    return esc(String(s).replace('T', ' ').slice(0, 10));
  }
  function fmtNum(v) {
    return (v === null || v === undefined) ? '-' : esc(v);
    return (v === null || v === undefined || v === '') ? '-' : esc(v);
  }
  // è¡¨æ ¼çª„列(数量 / æ—¥æœŸï¼‰ä¸æ¢è¡Œï¼Œé¿å…æ•°å­—被拆行
  function nw(html) {
    return '<span style="white-space:nowrap">' + html + '</span>';
  }
  function statusBadge(map, key) {
@@ -235,25 +419,44 @@
    return '<span class="badge ' + item[1] + '">' + esc(item[0]) + '</span>';
  }
  function card(title, countHtml, bodyHtml, opts) {
    opts = opts || {};
    var collapsible = !!opts.collapsible;
    var open = collapsible && !!opts.open;
    var hd = '<div class="card-hd"' +
      (collapsible ? ' onclick="this.parentNode.classList.toggle(\'open\')"' : '') +
      '><span class="bar"></span>' + esc(title) +
      (countHtml ? '<span class="count">' + countHtml + '</span>' : '') +
      '</div>';
    return '<div class="card' + (collapsible ? ' collapsible' : '') + (open ? ' open' : '') + '">' +
      hd + '<div class="card-bd">' + bodyHtml + '</div>' + '</div>';
  }
  function kv(k, v, dim) {
    return '<div class="kv"><span class="k">' + esc(k) + '</span><span class="v' + (dim ? ' dim' : '') + '">' + v + '</span></div>';
  }
  function isEmptyList(list) {
    return !list || list.length === 0;
  }
  // åŒºå—:绿竖条标题 + ç»†çº¿ + ç™½åº•描边面板
  function sec(title, countHtml, bodyHtml, opts) {
    opts = opts || {};
    var collapsible = !!opts.collapsible;
    var cls = 'sec' + (collapsible ? ' collapsible' : '') + (collapsible && opts.open ? ' open' : '');
    return '<section class="' + cls + '">' +
      '<div class="sec-hd"><span class="bar"></span><span class="ttl">' + esc(title) + '</span>' +
      '<span class="count">' + (countHtml || '') + '</span>' +
      (collapsible ? '<span class="caret">▾</span>' : '') +
      '</div>' +
      '<div class="sec-bd"><div class="panel">' + bodyHtml + '</div></div>' +
      '</section>';
  }
  function table(heads, rows) {
    var h = '<div class="tbl-wrap"><table class="mini-table"><thead><tr>';
    heads.forEach(function (t) { h += '<th>' + esc(t) + '</th>'; });
    h += '</tr></thead><tbody>';
    rows.forEach(function (cells) {
      h += '<tr>';
      cells.forEach(function (c) { h += '<td>' + c + '</td>'; });
      h += '</tr>';
    });
    return h + '</tbody></table></div>';
  }
  function li(titleHtml, timeHtml, bodyHtml) {
    return '<div class="li"><div class="li-hd">' + titleHtml +
      (timeHtml ? '<span class="li-time">' + timeHtml + '</span>' : '') +
      '</div><div class="li-bd">' + bodyHtml + '</div></div>';
  }
  // ========== æŸ¥è¯¢ ==========
@@ -270,8 +473,7 @@
        if (res.code === 0) {
          renderResult(res.data);
        } else {
          var msg = res.msg || '查询失败,请稍后重试';
          renderState('<div class="empty-tip error">' + esc(msg) + '</div>');
          renderState('<div class="empty-tip error">' + esc(res.msg || '查询失败,请稍后重试') + '</div>');
        }
      })
      .catch(function () {
@@ -284,27 +486,29 @@
    window.scrollTo({ top: 0, behavior: 'smooth' });
  }
  // ========== ç»“果渲染(信息区块模块化,后续新数据源在此追加区块) ==========
  // ========== ç»“果渲染 ==========
  function renderResult(d) {
    if (!d) {
      renderState('<div class="empty-tip error">未查询到该追溯码,请核对二维码或联系官方</div>');
      return;
    }
    // å·²ä½œåºŸçš„追溯码(破损替换后作废)不得作为有效产品流通,直接给出警示并停止展示溯源详情
    if (d.codeStatus === 'CANCELED') {
      renderState('<div class="empty-tip error">该追溯码已作废,非本公司有效产品,谨防假冒。<br>'
        + '如有疑问请联系官方核实。</div>');
      return;
    }
    var html = '';
    html += buildVerifyCard(d);
    html += buildItemCard(d);
    html += buildBatchCard(d);
    html += buildQualityCard(d);
    html += buildWorkerCard(d);
    html += buildInboundCard(d);
    html += buildPalletCard(d);
    html += buildBagCard(d);
    html += buildOptionalCard(d);
    html += sec('防伪查询', null, fakePanel(d), null);
    html += sec('追溯查询', null, tracePanel(d), null);
    // ç”Ÿäº§æ˜Žç»†æŠ˜å åŒºï¼šå›½æ ‡è¦æ±‚追溯网址可查询「种子加工批次」及「物流销售信息」,故对外展示
    // ï¼ˆåŠ å·¥æ‰¹æ¬¡ã€ç§å­æµå‘ã€åŠ å·¥æµè½¬ï¼›æŠ˜å ä¸å±•å¼€ï¼‰ã€‚è´¨æ£€/人员/托盘/袋码/采购来源等内部明细不对外展示,
    // å¦‚需一并展示,改为 detailSections(d, true) å³å¯ã€‚
    html += detailSections(d, false);
    STATE.innerHTML = html || '<div class="empty-tip">暂无追溯数据</div>';
    window.scrollTo({ top: 0, behavior: 'smooth' });
  }
  // åŒºå—0:正品验证(顶部,扫码优先展示验证结论与查询统计)
  function stripUrl(v) {
    if (!v) return v;
    if (/^https?:\/\//i.test(v)) {
@@ -314,195 +518,229 @@
    return v;
  }
  function buildVerifyCard(d) {
    var typeLabel = { BAG: '袋码', PALLET: '托盘码', BATCH: '批次码' }[d.sourceType] || '追溯码';
    var code = stripUrl(d.requestCode) || d.batchCode || d.tempCode || '-';
    var bagNo = '-';
    if (d.sourceType === 'BAG' && d.bags) {
      for (var i = 0; i < d.bags.length; i++) {
        if (d.bags[i].code === code) {
          bagNo = d.bags[i].bagNo;
          break;
        }
      }
    }
    var statsHtml = '<div class="verify-stats">';
    if (d.antiFake && d.antiFake.scanCount > 0) {
      statsHtml += kv('累计查询', esc(d.antiFake.scanCount) + ' æ¬¡') +
        kv('首次查询', fmtDate(d.antiFake.firstScanTime)) +
        kv('最近查询', fmtDate(d.antiFake.lastScanTime));
    } else {
      statsHtml += kv('累计查询', '暂无查询记录');
    }
    statsHtml += '</div>';
    return '<div class="card verify-card">' +
      '<div class="card-bd">' +
      '<div class="verify-title">正品验证</div>' +
      '<div class="verify-badge"><span class="badge-big green">✅ æ­£å“éªŒè¯é€šè¿‡</span></div>' +
      '<div class="verify-sub">该追溯码为正规渠道产品,溯源信息真实有效</div>' +
      kv('追溯类型', typeLabel) +
      kv(typeLabel, text(code)) +
      (d.sourceType === 'BAG' ? kv('袋号', bagNo) : '') +
      kv('批次号', text(d.batchCode)) +
      kv('品种', text(d.itemName)) +
      statsHtml +
      '</div></div>';
  // è¿½æº¯ç ï¼šæ‰«ç å€¼ â†’ æ‰¹æ¬¡ç  â†’ ä¸´æ—¶æ‰¹æ¬¡ç 
  function resolveCode(d) {
    return stripUrl(d.requestCode) || d.batchCode || d.tempCode || '';
  }
  // åŒºå—1:品种信息
  function buildItemCard(d) {
    if (!d.itemCode && !d.itemName && !d.itemSpecification) return '';
    return card('品种信息', null,
      kv('品种名称', text(d.itemName)) +
      kv('规格', text(d.itemSpecification)) +
      kv('品种编码', text(d.itemCode)),
      { collapsible: true, open: true }
    );
  }
  // åŒºå—1:防伪查询(验证状态条 + é˜²ä¼ªç¼–码 + å¯¹åº”产品 + æŸ¥è¯¢è®°å½•)
  function fakePanel(d) {
    var code = resolveCode(d);
    var af = d.antiFake || {};
    var n = Number(af.scanCount || 0);
    var repeated = n > 0;
    var icon = repeated
      ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M12 7.5v6"/><path d="M12 17h.01"/></svg>'
      : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12.5l5.5 5.5L20 6.5"/></svg>';
    var title = repeated ? '该防伪码已被查询 ' + n + ' æ¬¡' : '首次查询 Â· æœªè§å¼‚常';
    var desc = repeated
      ? '此码此前已被查询过,首次查询时间为 ' + fmtDate(af.firstScanTime) + '。若首次查询并非本人操作,请注意产品来源。'
      : '该防伪码此前没有查询记录,包装未被重复使用。';
    var name = [d.itemName, d.itemSpecification, d.itemUnitMeasureName].filter(Boolean).join(' ');
  // åŒºå—2:生产批次
  function buildBatchCard(d) {
    if (!d.batchCode && !d.tempCode) return '';
    var status = statusBadge(BATCH_STATUS, d.batchStatus);
    return card('生产批次', status,
      kv('批次号', text(d.batchCode)) +
      kv('状态', status) +
      kv('生产日期', fmtDate(d.produceDate)) +
      kv('产线', text(d.lineName ? (d.lineName + (d.lineCode ? ' / ' + d.lineCode : '')) : d.lineCode)) +
      kv('责任人', text(d.ownerUserName)) +
      kv('计划数量', fmtNum(d.planQuantity) === '-' ? '-' : fmtNum(d.planQuantity)) +
      kv('已生成袋码', fmtNum(d.bagQuantity)) +
      kv('已生成托盘', fmtNum(d.palletQuantity)) +
      kv('入库时间', fmtDate(d.inboundTime)) +
      (d.remark ? kv('备注', text(d.remark)) : ''),
      { collapsible: true, open: true }
    );
  }
  // åŒºå—3:种子质检
  function buildQualityCard(d) {
    var q = d.quality;
    if (!q) return '';
    var result = QUALITY_RESULT[q.result];
    var badge = result
      ? '<span class="badge-big ' + result[1] + '">' + esc(result[0]) + '</span>'
      : '<span class="badge-big gray">未知</span>';
    var rows = '';
    if (q.indicators && q.indicators.length) {
      rows = '<table class="mini-table"><thead><tr><th>指标</th><th>检测值</th><th>标准范围</th><th>结果</th></tr></thead><tbody>';
      q.indicators.forEach(function (i) {
        var range = (i.minValue !== null && i.minValue !== undefined && i.maxValue !== null && i.maxValue !== undefined)
          ? esc(i.minValue) + ' ~ ' + esc(i.maxValue) : '-';
        var pass = (i.pass === true)
          ? '<span class="badge green">合格</span>'
          : (i.pass === false ? '<span class="badge red">不合格</span>' : '<span class="badge gray">--</span>');
        rows += '<tr><td>' + text(i.indicatorName) + '</td><td>' + text(i.value) + '</td><td>' + range + '</td><td>' + pass + '</td></tr>';
      });
      rows += '</tbody></table>';
    }
    return card('种子质检', badge,
      kv('检验结论', badge) +
      kv('检验员', text(q.inspectorName)) +
      kv('检验时间', fmtDate(q.inspectedAt)) +
      (q.reason ? kv('不合格原因', text(q.reason)) : '') +
      (rows ? '<div style="margin-top:8px">' + rows + '</div>' : ''),
      { collapsible: true, open: false }
    );
  }
  // åŒºå—4:生产人员
  function buildWorkerCard(d) {
    if (isEmptyList(d.workers)) return '';
    var html = '<div class="pill-list">';
    d.workers.forEach(function (w) {
      var name = w.userName || '-';
      var type = w.workerTypeName || (WORKER_TYPE[w.workerType] || '');
      var color = { 10: 'var(--blue)', 20: 'var(--green)', 30: 'var(--primary-light)', 40: 'var(--red)', 50: 'var(--gray)' }[w.workerType] || 'var(--gray)';
      html += '<span class="pill"><span class="dot" style="background:' + color + '"></span>' +
        esc(type) + ':' + esc(name) + '</span>';
    });
    html += '</div>';
    return card('生产人员', null, html, { collapsible: true, open: false });
  }
  // åŒºå—5:扫码入库事件
  function buildInboundCard(d) {
    var e = d.inbound;
    if (!e) return '';
    return card('扫码入库', null,
      kv('事件号', text(e.eventId)) +
      kv('入库数量', fmtNum(e.quantity)) +
      kv('设备编码', text(e.deviceCode)) +
      kv('产线', text(e.lineCode)) +
      kv('入库时间', fmtDate(e.eventTime)),
      { collapsible: true, open: false }
    );
  }
  // åŒºå—6:托盘
  function buildPalletCard(d) {
    if (isEmptyList(d.pallets)) return '';
    var countHtml = '共 ' + esc(d.palletCount || d.pallets.length) + ' ä¸ª';
    var rows = '<table class="mini-table"><thead><tr><th>托盘码</th><th>序号</th><th>已装/可装</th></tr></thead><tbody>';
    d.pallets.forEach(function (p) {
      rows += '<tr><td>' + text(p.code) + '</td><td>' + fmtNum(p.palletNo) + '</td><td>' +
        fmtNum(p.bagCount) + ' / ' + fmtNum(p.capacity) + '</td></tr>';
    });
    rows += '</tbody></table>';
    return card('托盘信息', countHtml, rows, { collapsible: true, open: false });
  }
  // åŒºå—7:袋码(一袋一码)
  function buildBagCard(d) {
    if (d.bagTotal === null || d.bagTotal === undefined || d.bagTotal === 0) return '';
    var countHtml = '共 ' + esc(d.bagTotal) + ' ä¸ª' + (d.bags && d.bags.length < d.bagTotal ? '(展示前 ' + esc(d.bags.length) + ' ä¸ªï¼‰' : '');
    var rows = '<table class="mini-table"><thead><tr><th>袋码</th><th>袋号</th><th>托盘</th><th>状态</th></tr></thead><tbody>';
    (d.bags || []).forEach(function (b) {
      rows += '<tr><td>' + text(b.code) + '</td><td>' + fmtNum(b.bagNo) + '</td><td>' + text(b.palletCode) + '</td><td>' +
        statusBadge(BAG_STATUS, b.status) + '</td></tr>';
    });
    rows += '</tbody></table>';
    return card('袋码信息', countHtml,
      '<div class="scroll-list">' + rows + '</div>',
      { collapsible: true, open: false }
    );
  }
  // åŒºå—8:可选扩展(采购来源 / é”€å”®å޻向 / åŠ å·¥æµè½¬ï¼‰ï¼Œæœ‰æ•°æ®æ‰å±•ç¤º
  function buildOptionalCard(d) {
    var html = '';
    if (!isEmptyList(d.purchaseSources)) {
      var rows = '<table class="mini-table"><thead><tr><th>采购入库单</th><th>供应商</th><th>数量</th><th>日期</th></tr></thead><tbody>';
      d.purchaseSources.forEach(function (s) {
        rows += '<tr><td>' + text(s.receiptCode) + '</td><td>' + text(s.vendorName) + '</td><td>' + fmtNum(s.receivedQuantity) + '</td><td>' + fmtDate(s.receiptDate) + '</td></tr>';
      });
      rows += '</tbody></table>';
      html += card('采购来源', null, rows, { collapsible: true, open: false });
    }
    if (!isEmptyList(d.salesTargets)) {
      var rows2 = '<table class="mini-table"><thead><tr><th>销售出库单</th><th>客户</th><th>数量</th><th>日期</th></tr></thead><tbody>';
      d.salesTargets.forEach(function (s) {
        rows2 += '<tr><td>' + text(s.outCode || s.salesOrderCode || '') + '</td><td>' + text(s.customerName) + '</td><td>' + fmtNum(s.quantity) + '</td><td>' + fmtDate(s.outTime) + '</td></tr>';
      });
      rows2 += '</tbody></table>';
      html += card('销售去向', null, rows2, { collapsible: true, open: false });
    }
    if (!isEmptyList(d.processTraces)) {
      var rows3 = '<table class="mini-table"><thead><tr><th>节点</th><th>说明</th><th>时间</th></tr></thead><tbody>';
      d.processTraces.forEach(function (t) {
        rows3 += '<tr><td>' + text(t.nodeName) + '</td><td>' + text(t.description) + '</td><td>' + fmtDate(t.processTime) + '</td></tr>';
      });
      rows3 += '</tbody></table>';
      html += card('加工流转', null, rows3, { collapsible: true, open: false });
    }
    var html = '<div class="verify ' + (repeated ? 'warn' : 'ok') + '">' +
      '<span class="vico">' + icon + '</span>' +
      '<div class="vtxt"><div class="vt">' + title + '</div><div class="vs">' + desc + '</div></div>' +
      '</div>';
    html += '<div class="code-box"><span class="cl">防伪编码</span><span class="cv">' + esc(code || '-') + '</span></div>';
    html += kv('对应品种', text(name));
    html += d.batchCode
      ? kv('对应批次', text(d.batchCode))
      : kv('对应批次', '尚未绑定批次', true);
    html += '<div class="stat-grid">' +
      '<div class="stat span2 count"><div class="sl">该码累计被查询次数</div>' +
      '<div class="sv">' + n + '<span class="u">次</span></div></div>' +
      '<div class="stat"><div class="sl">首次查询时间</div><div class="sv">' + fmtDate(af.firstScanTime) + '</div></div>' +
      '<div class="stat"><div class="sl">最近查询时间</div><div class="sv">' + fmtDate(af.lastScanTime) + '</div></div>' +
      '</div>';
    html += '<div class="notice">' + (repeated
      ? '同一防伪码被重复查询,可能意味着包装被复制。请核对包装上印制的防伪编码与本页显示是否一致,如有疑问请联系官方核实。'
      : '请核对包装上印制的防伪编码与本页显示是否一致,并确认包装完好。') + '</div>';
    return html;
  }
  // ========== åˆå§‹åŒ– ==========
  // åŒºå—2:追溯查询(未录入批次时只展示品种信息 + æç¤ºï¼‰
  // å››è¦ç´ ï¼ˆå“ç§åç§° â†’ ç”Ÿäº§ç»è¥è€… â†’ å•元识别代码 â†’ è¿½æº¯ç½‘址)按国标顺序排列,其余信息排在其后
  function tracePanel(d) {
    var code = resolveCode(d);
    var name = [d.itemName, d.itemSpecification].filter(Boolean).join(' ');
    // ç”Ÿäº§ç»è¥è€…与追溯网址优先取查询结果(与标签印刷内容同源),退回页面配置
    var operatorName = d.operatorName || COMPANY_NAME;
    var traceUrl = d.traceUrl || TRACE_URL;
    var operatorLabel = IS_IMPORTED ? '进口商名称' : '生产经营者';
    // åªæœ‰è¢‹ç æ‰æ˜¯ã€Œæœ€å°é”€å”®å•元」,其码值才称「单元识别代码」;
    // æ‰˜ç›˜ç /批次码不属于最小销售单元标识,按实际类型展示,避免误导消费者
    var codeLabel = d.sourceType === 'BAG'
      ? '单元识别代码'
      : (SOURCE_TYPE[d.sourceType] ? SOURCE_TYPE[d.sourceType] + '(非最小销售单元)' : '追溯码');
    var unbound = d.batchBound === false;
    var batchRows = unbound
      ? '<div class="notice">该追溯码尚未录入生产批次,批次号、生产日期、产线等信息待完善;当前展示该码对应的品种信息。</div>'
      : kv('批次号', text(d.batchCode)) +
        kv('生产日期', fmtDate(d.produceDate)) +
        kv('产线', text(d.lineName || d.lineCode));
    return kv('品种名称', text(name)) +
      kv(operatorLabel, text(operatorName)) +
      kv(codeLabel, text(code || '-')) +
      kv('追溯网址', traceUrl
        ? '<a class="link" href="' + esc(traceUrl) + '">' + esc(traceUrl) + '</a>'
        : '-') +
      kv('计量单位', text(d.itemUnitMeasureName)) +
      kv('追溯类型', esc(SOURCE_TYPE[d.sourceType] || '追溯码')) +
      batchRows;
  }
  // åŒºå—3~:生产明细(折叠,有数据才展示)
  // full=true æ—¶å±•示全部明细(质检/人员/托盘/袋码/采购来源);
  // é»˜è®¤åªå±•示国标要求的「加工批次」与「物流销售(种子流向)」信息,其余区块不对外展示。
  function detailSections(d, full) {
    var html = '';
    // ç”Ÿäº§æ‰¹æ¬¡ä¿¡æ¯ï¼ˆå›½æ ‡è¦æ±‚:追溯网址应可查询种子加工批次)
    // æœªå½•入批次时不展示空区块,避免给出"无加工批次信息"的错误暗示
    if (d.batchBound !== false) {
      var batchBody =
        kv('批次号', text(d.batchCode)) +
        kv('批次状态', d.batchStatusName ? statusBadge({ v: [d.batchStatusName, 'blue'] }, 'v') : text(d.batchStatus)) +
        kv('生产日期', fmtDate(d.produceDate)) +
        kv('产线', text(d.lineName || d.lineCode)) +
        kv('计划数量', fmtNum(d.planQuantity)) +
        kv('已生成袋码', fmtNum(d.bagQuantity)) +
        kv('已生成托盘', fmtNum(d.palletQuantity)) +
        kv('入库时间', fmtDate(d.inboundTime)) +
        kv('备注', text(d.remark));
      html += sec('生产批次信息', null, batchBody, { collapsible: true, open: false });
    }
    // è´¨æ£€ä¿¡æ¯ï¼ˆä»…完整模式展示)
    if (full && d.quality) {
      var q = d.quality;
      var qBody =
        kv('质检结论', q.resultName ? '<span class="badge ' + (q.result === 1 ? 'green' : 'red') + '">' + esc(q.resultName) + '</span>' : '-') +
        kv('检验员', text(q.inspectorName)) +
        kv('检验时间', fmtDate(q.inspectedAt)) +
        (q.reason ? kv('不合格原因', text(q.reason)) : '');
      if (!isEmptyList(q.indicators)) {
        var rows = q.indicators.map(function (i) {
          var hasMin = i.minValue !== null && i.minValue !== undefined;
          var hasMax = i.maxValue !== null && i.maxValue !== undefined;
          var range = '-';
          if (hasMin && hasMax) range = fmtNum(i.minValue) + ' ~ ' + fmtNum(i.maxValue);
          else if (hasMin) range = '≥ ' + fmtNum(i.minValue);
          else if (hasMax) range = '≤ ' + fmtNum(i.maxValue);
          var pass = i.pass === null || i.pass === undefined
            ? '-'
            : '<span class="badge ' + (i.pass ? 'green' : 'red') + '">' + (i.pass ? '合格' : '不合格') + '</span>';
          return [text(i.indicatorName), fmtNum(i.value), range, pass];
        });
        qBody += '<div style="margin-top:8px">' + table(['检测项', '检测值', '标准范围', '结论'], rows) + '</div>';
      }
      html += sec('质检信息', null, qBody, { collapsible: true, open: false });
    }
    // ç”Ÿäº§äººå‘˜ï¼ˆä»…完整模式展示:涉及个人信息,不对外公开)
    if (full && !isEmptyList(d.workers)) {
      var wBody = d.workers.map(function (w) {
        return li('<span class="li-ttl">' + text(w.userName) + '</span>' +
          (w.workerTypeName ? '<span class="badge gray">' + esc(w.workerTypeName) + '</span>' : ''),
          null,
          w.remark ? kv('备注', text(w.remark)) : '');
      }).join('');
      html += sec('生产人员', d.workers.length + ' äºº', '<div class="scroll-list">' + wBody + '</div>', { collapsible: true, open: false });
    }
    // æ‰˜ç›˜ä¿¡æ¯ï¼ˆä»…完整模式展示)
    if (full && !isEmptyList(d.pallets)) {
      var pRows = d.pallets.map(function (p) {
        return [text(p.code), fmtNum(p.palletNo), fmtNum(p.bagCount), fmtNum(p.capacity)];
      });
      html += sec('托盘信息', '共 ' + fmtNum(d.palletCount || d.pallets.length) + ' ä¸ª',
        '<div class="scroll-list">' + table(['托盘码', '序号', '已装袋', '可装袋'], pRows) + '</div>',
        { collapsible: true, open: false });
    }
    // è¢‹ç åˆ—表(仅完整模式展示)
    if (full && !isEmptyList(d.bags)) {
      var bBody = d.bags.map(function (b) {
        return li('<span class="li-ttl">' + text(b.code) + '</span>' +
          (b.statusName ? '<span class="badge gray">' + esc(b.statusName) + '</span>' : ''),
          fmtDate(b.inboundTime),
          kv('袋号', fmtNum(b.bagNo)) + (b.palletCode ? kv('托盘码', text(b.palletCode)) : ''));
      }).join('');
      var bagCountText = '共 ' + fmtNum(d.bagTotal || d.bags.length) + ' è¢‹' +
        (Number(d.bagTotal) > d.bags.length ? '(显示前 ' + d.bags.length + ' æ¡ï¼‰' : '');
      html += sec('袋码列表', bagCountText, '<div class="scroll-list">' + bBody + '</div>', { collapsible: true, open: false });
    }
    // é‡‡è´­æ¥æºï¼ˆä»…完整模式展示)
    if (full && !isEmptyList(d.purchaseSources)) {
      var psRows = d.purchaseSources.map(function (s) {
        return [text(s.receiptCode), text(s.vendorName), nw(fmtNum(s.receivedQuantity)), nw(fmtDay(s.receiptDate))];
      });
      html += sec('采购来源', null, table(['采购入库单', '供应商', '数量', '日期'], psRows), { collapsible: true, open: false });
    }
    // ç§å­æµå‘信息(仅销售出库 bizType=119)
    var salesFlows = (d.seedFlows || []).filter(function (f) { return f.bizType === 119; });
    if (!isEmptyList(salesFlows)) {
      var sfBody = salesFlows.map(function (f) {
        var qty = (f.quantity === null || f.quantity === undefined) ? '-' : fmtNum(Math.abs(Number(f.quantity)));
        var wh = [f.warehouseName, f.locationName].filter(Boolean).join(' / ') || '-';
        return li(statusBadge(FLOW_DIRECTION, f.type) + '<span class="li-ttl">' + esc(FLOW_BIZ_TYPE[f.bizType] || '') + '</span>',
          fmtDate(f.transactionTime),
          kv('来源单号', text(f.bizCode)) +
          kv('客户', text(f.clientName)) +
          kv('数量', qty) +
          kv('仓库/库区', text(wh)));
      }).join('');
      html += sec('种子流向信息', null, '<div class="scroll-list">' + sfBody + '</div>', { collapsible: true, open: false });
    } else if (!isEmptyList(d.salesTargets)) {
      var stRows = d.salesTargets.map(function (s) {
        return [text(s.salesCode || s.salesOrderCode || ''), text(s.clientName), nw(fmtNum(s.quantity)), nw(fmtDay(s.salesDate))];
      });
      html += sec('种子流向信息', null, table(['销售出库单', '客户', '数量', '日期'], stRows), { collapsible: true, open: false });
    }
    // åŠ å·¥æµè½¬
    if (!isEmptyList(d.processTraces)) {
      var ptBody = d.processTraces.map(function (t) {
        return li('<span class="li-ttl">' + text(t.nodeName) + '</span>', fmtDate(t.processTime),
          t.description ? kv('说明', text(t.description)) : '');
      }).join('');
      html += sec('加工流转', null, '<div class="scroll-list">' + ptBody + '</div>', { collapsible: true, open: false });
    }
    return html;
  }
  // ========== äº¤äº’ ==========
  // æŠ˜å åŒºå—:标题点击展开/收起
  STATE.addEventListener('click', function (e) {
    var hd = e.target.closest ? e.target.closest('.sec.collapsible .sec-hd') : null;
    if (hd) hd.parentNode.classList.toggle('open');
  });
  SEARCH_BTN.addEventListener('click', function () { query(CODE_INPUT.value); });
  CODE_INPUT.addEventListener('keydown', function (e) { if (e.key === 'Enter') query(CODE_INPUT.value); });
  // å…¬å¸ä»‹ç»ï¼šå†…容超出可折叠高度时才显示“点击查看更多”
  (function initCompanyToggle() {
    var clip = document.getElementById('companyClip');
    var wrap = document.getElementById('companyMoreWrap');
    var more = document.getElementById('companyMore');
    if (clip.scrollHeight <= clip.clientHeight + 4) {
      clip.classList.add('open');
      wrap.style.display = 'none';
      return;
    }
    more.addEventListener('click', function () {
      var open = clip.classList.toggle('open');
      more.textContent = open ? '收起' : '点击查看更多';
    });
  })();
  // å…ˆæ‹‰å–页面配置(生产经营者名称、追溯网址),再执行查询;查询结果中同样带有这两项,互为兜底
  loadTraceConfig();
  var initialCode = new URLSearchParams(location.search).get('code') || '';
  CODE_INPUT.value = initialCode;
@@ -510,7 +748,6 @@
    query(initialCode);
  } else {
    renderState('<div class="empty-tip">扫码或输入追溯码,即可查询产品全程溯源信息</div>');
    CODE_INPUT.focus();
  }
})();
</script>