2 天以前 5f73eb1fb9d04040fe6ba377d5ae582024429f2f
feat(sales): 添加客户交易详情中的付款金额字段

- 在CustomerTransactionsVo中添加付款金额字段用于显示客户往来付款累计金额
- 在CustomerTransactionsDetailsVo中添加付款金额字段用于详细交易信息展示
- 修改CustomerMapper.xml查询语句增加付款金额相关统计逻辑
- 从payment_record表中获取供应商收款金额并关联到供应商交易查询中
- 优化会议参会人员ID解析逻辑,解决多条历史记录导致的异常问题
- 更新油罐基础信息实体类的时间戳更新策略
- 为采购记录添加台账ID支持精确反显功能
- 实现出库批次管理和销售出库自动生成发货台账功能
- 添加备件领用时的出库流水记录功能
- 完善采购台账新增时的备件库存回写机制
- 优化库存查询和发货单管理的相关功能实现
已添加31个文件
已修改29个文件
2799 ■■■■■ 文件已修改
doc/20260920_create_payment_record.sql 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260920_create_purchase_application.sql 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260920_rename_purchase_review_menu.sql 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260920_spare_parts_product_and_record.sql 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260920_stock_out_shipping_link.sql 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260920_储罐号带入库存前端联调文档.md 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260920_出库即发货与往来备件前端联调文档.md 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260920_采购申请模块前端联调文档.md 322 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/DbTool.java 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/db.sh 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/PaymentRecordController.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/PaymentRecordMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/PaymentRecord.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/PaymentRecordService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/PaymentRecordServiceImpl.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/OilDepot.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/TankInfo.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/RulesRegulationsManagementController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/MeetingServiceImpl.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/PurchaseApplicationStatusEnum.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/controller/SparePartsRecordController.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/mapper/SparePartsRecordMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/pojo/SpareParts.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/pojo/SparePartsRecord.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/SparePartsRecordService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/SparePartsRecordServiceImpl.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementDto.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutAdd.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutPageDto.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/vo/BatchNoOptionVo.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementRecordOutController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/mapper/ProcurementRecordOutMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/pojo/ProcurementRecordOut.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/ProcurementRecordOutService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordOutServiceImpl.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PurchaseApplicationController.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/PurchaseApplicationDto.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/PurchaseApplicationMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/PurchaseApplicationProductMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseApplication.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseApplicationProduct.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/PurchaseApplicationService.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseApplicationServiceImpl.java 346 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/StockInventoryExportData.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/SupplierManageMapper.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/SparePartsRecordMapper.xml 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ProcurementRecordMapper.xml 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ProcurementRecordOutMapper.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260920_create_payment_record.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
# å¾€æ¥ä»˜æ¬¾å›žæ¬¾æµæ°´è¡¨
# å®¢æˆ·å¾€æ¥å– party_type=1 and direction=1 ä½œä¸ºã€Œä»˜æ¬¾ã€åˆ—
# ä¾›åº”商往来取 party_type=2 and direction=2 ä½œä¸ºã€Œå›žæ¬¾ã€åˆ—
drop table if exists payment_record;
create table payment_record
(
    id             bigint auto_increment primary key,
    party_type     tinyint        not null comment '往来方类型 1客户 2供应商',
    party_id       bigint         not null comment '客户id或供应商id',
    direction      tinyint        not null comment '方向 1付款 2回款',
    amount         decimal(18, 2) not null default 0 comment '金额',
    record_date    date           null comment '发生日期',
    payment_method varchar(64)    null comment '付款/回款方式',
    remark         varchar(500)   null comment '备注',
    tenant_id      bigint         null comment '租户ID',
    dept_id        bigint         null comment '部门ID',
    create_user    int            null comment '创建人ID',
    create_time    datetime       null comment '创建时间',
    update_user    int            null comment '更新人ID',
    update_time    datetime       null comment '更新时间',
    index idx_party (party_type, party_id),
    index idx_direction (direction)
);
doc/20260920_create_purchase_application.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
#采购申请单表
drop table if exists purchase_application;
create table purchase_application
(
    id                    bigint auto_increment primary key,
    application_no        varchar(50)    not null comment '申请单号',
    supplier_id           bigint         null comment '供应商id',
    supplier_name         varchar(100)   null comment '供应商名称',
    project_name          varchar(100)   null comment '项目名称',
    sales_contract_no     varchar(50)    null comment '销售合同号',
    sales_ledger_id       int            null comment '关联销售台账主表主键',
    applicant_id          bigint         null comment '申请人id',
    applicant_name        varchar(50)    null comment '申请人姓名',
    apply_date            date           null comment '申请日期',
    expected_arrival_date date           null comment '期望到货日期',
    contract_amount       decimal(10, 2) null comment '申请金额(含税总价)',
    payment_method        varchar(255)   null comment '付款方式',
    remarks               text           null comment '备注',
    status                tinyint        not null default 0 comment '状态 0草稿 1已生成订单',
    purchase_ledger_id    bigint         null comment '转订单后生成的采购台账id',
    convert_time          datetime       null comment '转订单时间',
    tenant_id             bigint         null comment '租户ID',
    dept_id               bigint         null comment '部门ID',
    create_user           bigint         null comment '创建人ID',
    create_time           datetime       null comment '创建时间',
    update_time           datetime       null comment '更新时间',
    index idx_application_no (application_no),
    index idx_supplier_id (supplier_id),
    index idx_purchase_ledger_id (purchase_ledger_id)
);
#采购申请产品明细表
drop table if exists purchase_application_product;
create table purchase_application_product
(
    id                        bigint auto_increment primary key,
    purchase_application_id   bigint         not null comment '采购申请单id',
    product_id                bigint         null comment '产品id',
    product_model_id          bigint         null comment '产品规格id',
    product_category          varchar(100)   null comment '产品大类',
    specification_model       varchar(100)   null comment '规格型号',
    unit                      varchar(20)    null comment '单位',
    quantity                  decimal(18, 2) not null default 0 comment '数量',
    tax_rate                  decimal(5, 0)  null comment '税率',
    tax_inclusive_unit_price  decimal(18, 2) null comment '含税单价',
    tax_inclusive_total_price decimal(18, 2) null comment '含税总价',
    tax_exclusive_total_price decimal(18, 2) null comment '不含税总价',
    invoice_type              varchar(50)    null comment '发票类型',
    is_checked                tinyint(1)     not null default 1 comment '是否推送质检',
    remark                    varchar(255)   null comment '备注',
    tenant_id                 bigint         null comment '租户ID',
    dept_id                   bigint         null comment '部门ID',
    create_user               bigint         null comment '创建人ID',
    create_time               datetime       null comment '创建时间',
    update_time               datetime       null comment '更新时间',
    index idx_purchase_application_id (purchase_application_id)
);
doc/20260920_rename_purchase_review_menu.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
-- é‡‡è´­å®¡æ ¸ èœå•改名为 é‡‡è´­ç”³è¯·ï¼ˆåŽ»æŽ‰å®¡æ ¸è¯­ä¹‰ï¼‰
-- å‰ç«¯éœ€è¦åŒæ­¥æ–°å¢žé¡µé¢ï¼šprocurementManagement/purchaseApplication/index
UPDATE sys_menu
SET menu_name = '采购申请',
    path      = 'purchaseApplication',
    component = 'procurementManagement/purchaseApplication/index'
WHERE menu_id = 2355;
doc/20260920_spare_parts_product_and_record.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
# å¤‡ä»¶å…³è”产品数据(产品大类 + è§„格型号),使备件可走采购台账明细
alter table spare_parts
    add column product_id       bigint null comment '关联产品大类id',
    add column product_model_id bigint null comment '关联产品规格id';
alter table spare_parts
    add index idx_product_model_id (product_model_id);
# å¤‡ä»¶å‡ºå…¥åº“流水
drop table if exists spare_parts_record;
create table spare_parts_record
(
    id             bigint auto_increment primary key,
    spare_parts_id bigint         not null comment '备件id',
    direction      tinyint        not null comment '方向 1入库 2出库',
    quantity       decimal(18, 2) not null default 0 comment '数量',
    source_type    tinyint        null comment '来源 1采购入库 2维修领用 3保养领用',
    source_id      bigint         null comment '来源单据id',
    source_no      varchar(64)    null comment '来源单号',
    remark         varchar(500)   null comment '备注',
    tenant_id      bigint         null comment '租户ID',
    dept_id        bigint         null comment '部门ID',
    create_user    int            null comment '创建人ID',
    create_time    datetime       null comment '创建时间',
    update_user    int            null comment '更新人ID',
    update_time    datetime       null comment '更新时间',
    index idx_spare_parts_id (spare_parts_id),
    index idx_source (source_type, source_id)
);
doc/20260920_stock_out_shipping_link.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
# å‡ºåº“台账与发货台账关联:油品出库即发货
# procurement_record_out å¢žåŠ å‘è´§å•å…³è”ä¸Žå‡ºåº“æ‰¹æ¬¡ï¼ˆæ‰£åº“å­˜å¿…éœ€ï¼‰
alter table procurement_record_out
    add column shipping_info_id bigint      null comment '自动生成的发货台账id',
    add column batch_no         varchar(64) null comment '出库批次(扣库存用)';
alter table procurement_record_out
    add index idx_shipping_info_id (shipping_info_id);
doc/20260920_´¢¹ÞºÅ´øÈë¿â´æÇ°¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
# å‚¨ç½å·ï¼ˆå‚¨ç½åˆ†ç±»ï¼‰å¸¦å…¥åº“å­˜ å‰ç«¯è”调文档
更新时间:2026-09-20
适用版本:本次后端变更后
对应需求:需求清单第 12 æ¡
实现方式说明:**不是新写代码,而是把上游 `dev_New_pro` ä¸Šå·²æœ‰çš„实现拿过来**——cherry-pick äº† `d8c86797a`(油库/储罐模块 + åº“存出入库字段全链路)和 `50921bdc2` çš„库存排序修复,再补了 2 å¤„该提交漏掉的展示面(库存管理列表、导出 Excel)。因此**库表列和菜单后端此前在本分支是缺失的**:菜单 2353/2354 ç‚¹å¼€ä¸€ç›´æŠ¥é”™ï¼Œæœ¬æ¬¡ä¸€å¹¶ä¿®å¥½ã€‚
---
## 1. å˜æ›´èŒƒå›´
| å†…容 | å½±å“é¢ |
| --- | --- |
| æ²¹åº“信息、储罐信息两个主数据模块后端补齐 | èœå• 2353 æ²¹åº“信息、2354 å‚¨ç½ä¿¡æ¯ |
| å‚¨ç½å· `tankNo` å¸¦å…¥åº“å­˜ | åº“存管理列表、库存详情/批号明细、导出 Excel、入库管理列表 |
| å…¥åº“记录新增油品/储罐/物流/过磅等 28 ä¸ªå­—段 | æ–°å¢žå…¥åº“表单、入库管理列表 |
| å‡ºåº“记录新增油品/储罐/物流/过磅等字段 | æ–°å¢žå‡ºåº“表单、出库记录列表 |
| æ–°å¢žã€Œä»…创建出库记录」接口 | æ²¹å“å‡ºåº“表单 |
> **口径**:需求方确认「储罐号就是储罐分类」,不另建分类字段。储罐的「大类」在 `tank_info.category`,储罐号是 `tank_info.tank_no`,库存各页展示的是后者。
---
## 2. å‰æï¼šå…ˆå»ºä¸»æ•°æ®ï¼Œå¦åˆ™å‚¨ç½å·æ°¸è¿œæ˜¯ç©º
储罐号是从**入库记录**带出来的:`stock_inventory(product_model_id + batch_no)` â†’ `stock_in_record(product_model_id + batch_no)` â†’ `tank_no`。所以**必须先有储罐主数据、且入库时选了储罐**,库存页才有值。
### 2.1 æ²¹åº“信息
前缀 `/basic/oilDepot`。
| æŽ¥å£ | æ–¹æ³• | è¯´æ˜Ž |
| --- | --- | --- |
| `/basic/oilDepot/list` | GET | åˆ†é¡µæŸ¥è¯¢ï¼Œè¿”回 `R.data.records` |
| `/basic/oilDepot/all` | GET | å…¨é‡ï¼Œä¾›ä¸‹æ‹‰é€‰æ‹© |
| `/basic/oilDepot/{id}` | GET | è¯¦æƒ… |
| `/basic/oilDepot/add` | POST | æ–°å¢ž |
| `/basic/oilDepot/update` | POST | ä¿®æ”¹ï¼Œå¿…须带 `id` |
| `/basic/oilDepot/del` | DELETE | æ‰¹é‡åˆ é™¤ï¼Œbody ä¸º id æ•°ç»„ |
字段:`id`、`depotName`(油库名称)、`chargePerson`(负责人)、`phone`、`enableDate`(启用日期 `yyyy-MM-dd`)、`company`(所属公司)。
### 2.2 å‚¨ç½ä¿¡æ¯
前缀 `/basic/tankInfo`,接口形态与油库完全一致(`list` / `all` / `{id}` / `add` / `update` / `del`)。
字段:`id`、`tankNo`(**储罐号**,如 `01#`)、`category`(大类,如 `成品油`)、`oilProduct`(油品,如 `柴#0`)、`capacity`(罐容)、`remaining`(库存余量 m³)、`tenantInfo`(租客信息)。
**前端**:储罐信息表单的「大类」「油品」建议做成下拉(当前库内值为 å¤§ç±»=`成品油`、油品=`柴#0`/`柴#-35`/`æ±½#92`/`æ±½#95`,可与产品规格口径对齐后再定)。
---
## 3. æ–°å¢žå…¥åº“:字段与接口(**有个坑,必看**)
### 3.1 åªæœ‰ `addStockInRecordOnly` ä¼šé€ä¼ æ²¹å“/储罐字段
| æŽ¥å£ | æ˜¯å¦é€ä¼ æ²¹å“/储罐/物流/过磅字段 |
| --- | --- |
| `POST /stockInventory/addStockInRecordOnly` | **是**(28 ä¸ªå­—段全透传,并按 id åæŸ¥å›žå¡«æ²¹åº“名称、储罐号) |
| `POST /stockInventory/addstockInventory` | **否**(只写 recordType/batchNo/productModelId/数量/备注/预警数量,传了这些字段会被静默丢弃) |
两者都会写一条 `stock_in_record`;区别是 `addStockInRecordOnly` **不动** `stock_inventory`(仅创建记录),`addstockInventory` ä¼šåŒæ—¶å¢žå‡åº“存。**新增入库表单若要存下储罐号,必须走 `addStockInRecordOnly`。**
> è¿™ä¸€ç‚¹æ˜¯ä¸Šæ¸¸å®žçŽ°çš„åŽŸæ ·è¡Œä¸ºï¼Œæœ¬æ¬¡æœªæ”¹åŠ¨ã€‚è‹¥ä¸šåŠ¡ä¸Šæ–°å¢žå…¥åº“éœ€è¦ã€Œæ—¢å­˜æ²¹å“/储罐字段、又加库存」,需另行确认调用顺序(先建记录再加库存会写两条记录),**属需与需求方确认的联调点**。
### 3.2 è¯·æ±‚体字段(`StockInventoryDto`)
必填沿用原有:`productModelId`、`qualitity`;`batchNo` ä¸ºç©ºæ—¶åŽç«¯æŒ‰ `yyyyMMdd-产品编码-序号` è‡ªåŠ¨ç”Ÿæˆã€‚
新增字段:
| åˆ†ç»„ | å­—段 | ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- | --- |
| æ²¹åº“储罐 | `oilDepotId` | Long | æ²¹åº“ id;传了 id ä½†æ²¡ä¼ åç§°æ—¶åŽç«¯åæŸ¥å›žå¡« |
| | `oilDepotName` | String | æ²¹åº“名称(可只传 id) |
| | `inboundCategory` | String | å…¥åº“类别:自购入库 / ä»£å‚¨å…¥åº“ |
| | `tankId` | Long | å‚¨ç½ id;传了 id ä½†æ²¡ä¼ ç½å·æ—¶åŽç«¯åæŸ¥å›žå¡« |
| | `tankNo` | String | **储罐号**(可只传 `tankId`) |
| | `inboundTime` | String | å…¥åº“æ—¶é—´ `yyyy-MM-dd HH:mm:ss` |
| | `oilProduct` | String | æ²¹å“ï¼šæ±½#92 / æ±½#95 / æŸ´#0 / æŸ´#-35 |
| | `originalQuantity` | BigDecimal | åŽŸåŽ‚æ•°é‡(吨) |
| å®¢æˆ· | `customerId` | Long | ä¼ äº† id ä½†æ²¡ä¼ åç§°æ—¶åŽç«¯åæŸ¥å›žå¡« |
| | `customerName` / `contactPerson` / `contactPhone` | String | å®¢æˆ·åç§° / å¯¹æŽ¥äºº / ç”µè¯ |
| ç‰©æµ | `driverName` / `driverPhone` | String | å¸æœºå§“名 / ç”µè¯ |
| | `escortName` / `escortPhone` | String | æŠ¼è¿å‘˜å§“名 / ç”µè¯ |
| | `truckPlateNo` / `trailerPlateNo` | String | è½¦ç‰Œå· / è½¦æŒ‚牌号 |
| å¸æ²¹æ“ä½œ | `tankInspectionStatus` | String | è½¦ä½“/铅封检验:正常 / å¼‚常 |
| | `unloadingCranePosition` | String | å¸è½¦é¹¤ä½ï¼š1#撬装 / 2#撬装 / ä¸´æ—¶æ³µ |
| | `density` | String | å¯†åº¦ kg/L |
| | `unloadPhotos` | **数组** | å¸æ²¹å…³é”®çŽ¯èŠ‚ä¸Žè½¦è¾†å…³é”®ä½ç½®æ‹ç…§ |
| è¿‡ç£… | `grossWeight` / `tareWeight` / `netWeight` | BigDecimal | æ¯›é‡ / çš®é‡ / å‡€é‡(吨) |
| | `lossQuantity` | BigDecimal | äºé‡(吨) |
| | `actualVolume` | BigDecimal | å®žé™…入库体积(升) |
| | `poundPhotos` | **数组** | ç£…单上传 |
**照片字段的入参/出参形态不一致,务必注意**:
- **入参**传数组(后端用 `JSON.toJSONString` å­˜åº“)
- **出参**是 JSON **字符串**(`"[{\"uidFilename\":...}]"`),前端需 `JSON.parse` åŽå†æ¸²æŸ“
- å‡ºå‚里的 URL ä¼šè¿‡æœŸï¼ˆç­¾å 2 å°æ—¶ï¼‰ï¼Œæ‰€ä»¥**批号明细接口返回前会重新签发**(见 4.2);其他接口若直接返回库里的旧串,图片可能加载失败
---
## 4. å››å¤„展示落点
### 4.1 åº“存管理列表 â€”— `GET /stockInventory/pageListCombinedStockInventory`
返回 `R.data.records`。本次新增列(**只多不少,原有字段不变**):
| å­—段 | è¯´æ˜Ž |
| --- | --- |
| `tankNo` | **储罐号**(本次需求主字段) |
| `oilProduct` | æ²¹å“ |
| `tankInspectionStatus` / `unloadingCranePosition` | è½¦ä½“铅封检验 / å¸è½¦é¹¤ä½ |
| `density` | å¯†åº¦ |
| `grossWeight` / `tareWeight` / `netWeight` / `lossQuantity` / `actualVolume` | æ¯›é‡/皮重/净重/亏量/实际体积 |
| `unloadPhotos` / `poundPhotos` | å¸æ²¹æ‹ç…§ / ç£…单(JSON å­—符串) |
> è¯¥æŽ¥å£**不返回** `oilDepotName`、`inboundCategory`、`inboundTime`、`tankId`——要这些请用 4.2 çš„详情接口。
>
> **取数口径**:本页按**规格型号**聚合(一行一个规格),所以 `tankNo` å–的是该规格**最近一次入库记录**的储罐。同规格分布在多个罐时只显示一个,详见第 6 èŠ‚é™åˆ¶ã€‚
### 4.2 åº“存详情 / æ‰¹å·æ˜Žç»† â€”— `GET /stockInventory/getBatchNoQty`
按 `productModelId`(可选 `productId`)查,返回 `R.data.records`。这一页是**按批次精确关联**的,储罐号准确。
新增入库侧列:
| å­—段 | è¯´æ˜Ž |
| --- | --- |
| `tankNo` | **储罐号** |
| `oilDepotName` / `oilProduct` / `inboundCategory` / `inboundTime` | æ²¹åº“名称 / æ²¹å“ / å…¥åº“类别 / å…¥åº“æ—¶é—´ |
| `tankInspectionStatus` / `unloadingCranePosition` / `density` | æ£€éªŒ / é¹¤ä½ / å¯†åº¦ |
| `grossWeight` / `tareWeight` / `netWeight` / `lossQuantity` / `actualVolume` | è¿‡ç£…数据 |
| `unloadPhotos` / `poundPhotos` | JSON å­—符串,**已重新签发 URL,可直接用** |
新增出库侧列(取该规格**最新一条出库记录**,统一 `out` å‰ç¼€ï¼‰ï¼š
`outCategory`(出库类别:客存/代储/销售)、`outDate`、`outTonnage`、`outCustomerName`、`outContactPerson`、`outContactPhone`、`outOilDepotName`、`outTankNo`、`outOilProduct`、`outDensity`、`outDriverName`、`outDriverPhone`、`outEscortName`、`outEscortPhone`、`outTruckPlateNo`、`outTrailerPlateNo`、`outGrossWeight`、`outTareWeight`、`outNetWeight`、`outLossQuantity`、`outActualVolume`、`outPoundPhotos`
另有两个未加前缀、但同属出库侧的列:`loadingInspectionStatus`(装车检验)、`loadingCranePosition`(装车鹤位)、`loadingPhotos`(装车拍照,JSON å­—符串,同样已重签 URL)。
**排序行为变更(`50921bdc2`)**:排序由 `order by batch_no` æ”¹ä¸º **`order by MAX(update_time) desc`**,即**按更新时间倒序**。前端若原本依赖批号顺序展示,需要改成显式排序或接受新顺序。
### 4.3 å¯¼å‡º Excel â€”— `POST /stockInventory/exportStockInventory`
导出文件**末尾新增「储罐号」列**。
- è¯¥åˆ—在后端标注为「仅导出」,所以**下载导入模板(`POST /stockInventory/downloadStockInventory`)不会出现储罐号列**
- **导入不支持填储罐号**:导入仍按「产品名称 | è§„格」匹配产品,储罐号不参与解析。若业务需要批量导入储罐号,需另提需求
### 4.4 å…¥åº“管理列表 â€”— `GET /stockInRecord/listPage`
底层用 `select sir.*`,所以本次入库记录新增的字段**全部自动带出**,前端直接用即可:
`tankNo`、`tankId`、`oilDepotId`、`oilDepotName`、`inboundCategory`、`inboundTime`、`oilProduct`、`originalQuantity`、`customerId`、`customerName`、`contactPerson`、`contactPhone`、`driverName`、`driverPhone`、`escortName`、`escortPhone`、`truckPlateNo`、`trailerPlateNo`、`tankInspectionStatus`、`unloadingCranePosition`、`density`、`unloadPhotos`、`grossWeight`、`tareWeight`、`netWeight`、`lossQuantity`、`actualVolume`、`poundPhotos`
### 4.5 é™„带:出库记录列表 â€”— `GET /stockOutRecord/listPage`
同样 `sor.*`,出库侧新增字段自动带出:`tankNo`、`tankId`、`oilDepotId`、`oilDepotName`、`outCategory`、`outDate`、`outTonnage`、`oilProduct`、`customerId`、`customerName`、`contactPerson`、`contactPhone`、`loadingInspectionStatus`、`loadingCranePosition`、`loadingPhotos`、`density`、`grossWeight`、`tareWeight`、`netWeight`、`lossQuantity`、`actualVolume`、`poundPhotos`、`driverName`、`driverPhone`、`escortName`、`escortPhone`、`truckPlateNo`、`trailerPlateNo`
---
## 5. æ–°å¢žæŽ¥å£ï¼šä»…创建油品出库记录
```
POST /stockInventory/addOilOutRecordOnly
```
请求体为 `StockOutRecordDto`,字段与 4.5 åˆ—出的出库侧字段一致(新增字段同表)。行为:
- `stockOutNum` ç”± `outTonnage` å›žå¡«ï¼Œ`approvalStatus` ç½® `0`(待审批)
- `recordType` ç”±åŽç«¯å›ºå®šä¸ºè‡ªå®šä¹‰å‡ºåº“类型,`recordId` å›ºå®š `0`,**前端不要传**
- **不调整库存**,只落一条出库记录
---
## 6. å·²çŸ¥é™åˆ¶ï¼ˆè”调前需知情)
1. **库存管理列表的储罐号是「规格级」而非「批次级」**。该页一行一个规格,`batch_no` æ˜¯ `GROUP_CONCAT`,所以储罐号只能取该规格最近一条入库记录的值。**同一规格分布在多个储罐时,列表只会显示最近入库的那个罐**。要按批次看准确的罐,用 4.2 çš„æ‰¹å·æ˜Žç»†ã€‚
2. **现有数据大多是空的**。当前库内 12 æ¡å…¥åº“记录里只有 3 æ¡æœ‰ `tank_no`(历史入库未采集)。所以联调时库存各页大部分行的储罐号会是空——**这是数据现状,不是接口问题**。需要业务补录,或重新走一遍带储罐的入库。
3. å¯¼å‡ºçš„储罐号列位置在**最后**(备注之后),若前端/业务有列顺序约定需注意。
4. `addstockInventory` ä¸é€ä¼ æ²¹å“/储罐字段(见 3.1),这是本次最容易踩的点。
5. å‰ç«¯ç•Œé¢å±‚无法在本后端仓库验证(无 `web/`、`ui/`),以下清单需前端仓库配合执行。
---
## 7. è”调自查清单
- [ ] èœå• 2353 æ²¹åº“信息、2354 å‚¨ç½ä¿¡æ¯èƒ½æ‰“开并增删改查(此前后端缺失,点开报错)
- [ ] å‚¨ç½ä¿¡æ¯èƒ½ä¿å­˜ `tankNo`、`category`、`oilProduct`、`capacity`、`remaining`
- [ ] æ–°å¢žå…¥åº“表单:油库/储罐做成下拉(`/basic/oilDepot/all`、`/basic/tankInfo/all`),选储罐后 `tankNo` è‡ªåŠ¨å¸¦å‡º
- [ ] æ–°å¢žå…¥åº“提交后,入库管理列表该行 `tankNo` æœ‰å€¼
- [ ] èµ°çš„æ˜¯ `addStockInRecordOnly`(用 `addstockInventory` æäº¤åˆ™ `tankNo` ä¸ºç©º â€”— ç”¨æ¥åå‘确认 3.1)
- [ ] åº“存管理列表出现「储罐号」列,且与同规格最近一次入库的罐一致
- [ ] åº“存详情/批号明细出现「储罐号」,且**同一规格不同批次能显示不同罐**
- [ ] åº“存详情排序为更新时间倒序
- [ ] å¯¼å‡ºåº“å­˜ Excel æœ«å°¾æœ‰ã€Œå‚¨ç½å·ã€åˆ—且有值
- [ ] ä¸‹è½½å¯¼å…¥æ¨¡æ¿**不含**储罐号列;用该模板导入仍能按「产品名称|规格」匹配成功
- [ ] ç…§ç‰‡å­—段:上传时传数组,回显时能正确解析 JSON å­—符串,图片可打开
- [ ] å‡ºåº“记录列表 `/stockOutRecord/listPage` èƒ½å–到 `tankNo` ç­‰å‡ºåº“侧新字段
doc/20260920_³ö¿â¼´·¢»õÓëÍùÀ´±¸¼þǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,235 @@
# å‡ºåº“即发货 / å¾€æ¥ä»˜æ¬¾å›žæ¬¾ / å¤‡ä»¶æµæ°´ å‰ç«¯è”调文档
更新时间:2026-09-20
适用版本:本次后端变更后
对应需求:需求清单第 11、13、14、15 æ¡
---
## 1. å˜æ›´èŒƒå›´
| éœ€æ±‚ | å†…容 | å½±å“é¢ |
| --- | --- | --- |
| 13 | æ²¹å“**销售出库即发货**:出库时自动生成一张「已发货」发货台账,不发起发货审批 | å‡ºåº“表单、出库台账列表 |
| 11 | å®¢æˆ·/供应商往来各**新增一个业务字段**(客户「付款」、供应商「回款」)并可手工维护流水 | å®¢æˆ·å¾€æ¥é¡µã€ä¾›åº”商往来页 |
| 15 | å¤‡ä»¶**关联产品规格**,采购入库自动累加备件库存,并新增备件出入库流水 | å¤‡ä»¶ç»´æŠ¤é¡µã€å¤‡ä»¶æµæ°´é¡µ |
| 14 | æ–°å¢žå…¥åº“选采购订单后**精确反显明细**(修复后端 SQL) | æ–°å¢žå…¥åº“页 |
DDL å·²æ‰§è¡Œï¼ˆ`doc/20260920_stock_out_shipping_link.sql`、`doc/20260920_create_payment_record.sql`、`doc/20260920_spare_parts_product_and_record.sql`)。
---
## 2. éœ€æ±‚ 13:出库即发货
### 2.1 è§„则说明
- ä»… **`type = 2`(销售出库)** è§¦å‘自动发货;`type = 1` é‡‡è´­å‡ºåº“、`type = 3` è‡ªå®šä¹‰å‡ºåº“不受影响。
- è‡ªåŠ¨ç”Ÿæˆçš„å‘è´§å°è´¦ `status` ç›´æŽ¥ä¸º **`已发货`**,不进入审批,也不出现在「待审核」列表。
- å‘货单号由后端生成,格式 `SH` + `yyyyMMdd` + å½“æ—¥ 3 ä½æµæ°´ï¼Œå¦‚ `SH20260920001`。
- **同一销售产品允许分批多次发货**:每张出库单对应一张新的发货单,各自扣减一次库存。
- å‡ºåº“单与发货单通过 `procurement_record_out.shipping_info_id` å…³è”ï¼›**删除出库单会连带冲销发货单并回滚已扣库存**。
### 2.2 å‡ºåº“接口:新增必填 `batchNo`
`POST /stockmanagement/stockout`
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
| --- | --- | --- | --- |
| `type` | Integer | æ˜¯ | `2` é”€å”®å‡ºåº“时才触发自动发货 |
| `batchNo` | String | **销售出库必填** | å‡ºåº“批次。为空时后端抛「请选择出库批次」 |
| `salesLedgerProductId` | Long | é”€å”®å‡ºåº“å¿…å¡« | éœ€èƒ½åæŸ¥åˆ° `sales_ledger_product` ä¸” `type = 1`(销售) |
| `productModelId` | Long | æ˜¯ | è§„格型号 id |
| `quantity` | String | æ˜¯ | å‡ºåº“数量 |
| `id` | Long | æ˜¯ | å¯¹åº”çš„ `procurement_record_storage` æ˜Žç»† id |
**批次不允许留空**:系统无先进先出,库存按批次管理,后端按 `productModelId + batchNo` æ‰£å‡ï¼Œæ‰¹æ¬¡ä¸å¯¹ä¼šæŠ¥ã€Œåº“存记录不存在」。
### 2.3 æ–°å¢žï¼šæ‰¹æ¬¡å¯é€‰åˆ—表
```
GET /stockmanagement/batchNoList?productModelId={规格型号id}
```
响应:
```json
{
  "code": 200,
  "data": [
    { "productModelId": 405, "batchNo": "20260915-001-001", "availableQuantity": 33.00 },
    { "productModelId": 405, "batchNo": "CG20260825001-192", "availableQuantity": 10000.00 }
  ]
}
```
- `availableQuantity` = åº“存量 âˆ’ é”å®šæ•°é‡ï¼Œå·²è¿‡æ»¤æŽ‰ â‰¤ 0 çš„æ‰¹æ¬¡ã€‚
- å‰ç«¯åœ¨å‡ºåº“表单选择规格型号后调用,把 `batchNo` åšæˆä¸‹æ‹‰ï¼Œå¹¶æŠŠå¯ç”¨é‡å±•示给用户。
### 2.4 å‡ºåº“台账列表新增发货单号
`GET /stockmanagement/listPageByProduct` è¿”回体在原有字段基础上**新增**:
| å­—段 | è¯´æ˜Ž |
| --- | --- |
| `shippingNo` | è‡ªåŠ¨ç”Ÿæˆçš„å‘è´§å•å·ï¼Œå¦‚ `SH20260920001`;非销售出库为 `null` |
> `GET /stockmanagement/listPage`(采购出库 type=1)**不返回**该字段,采购出库不发货。
### 2.5 åˆ é™¤å‡ºåº“单
`POST /stockmanagement/del` è¡Œä¸ºå·²å˜æ›´ï¼šè‹¥è¯¥å‡ºåº“单已自动生成发货单,后端会先删除发货单与明细、回滚库存,再删除出库单。前端**无需改动调用方式**,但提示文案可说明「会同时冲销发货记录」。
---
## 3. éœ€æ±‚ 11:往来付款 / å›žæ¬¾
### 3.1 è¯­ä¹‰
| å¾€æ¥ | æ–°å¢žå­—段 | å–值来源 |
| --- | --- | --- |
| å®¢æˆ·å¾€æ¥ | `paymentAmount`(**付款**) | `payment_record` ä¸­ `party_type=1 and direction=1` çš„合计 |
| ä¾›åº”商往来 | `receiptAmount`(**回款**) | `payment_record` ä¸­ `party_type=2 and direction=2` çš„合计 |
**只有一个方向是手工流水**:客户侧的「收款金额」`receiptPaymentAmount`、供应商侧的「付款金额」`paymentAmount` ä»æ¥è‡ªåŽŸæœ‰çš„é‡å•æ®è¡¨ï¼Œ**计算口径未变**;`receiptableAmount`(应收)与 `payableAmount`(应付)公式**保持不变**,新字段只做展示,不参与余额计算。
### 3.2 è¿”回字段新增
| æŽ¥å£ | æ–°å¢žè¿”回字段 |
| --- | --- |
| `GET /metricStatistics/customewTransactions` | `paymentAmount` |
| `GET /metricStatistics/customewTransactionsDetails` | `paymentAmount` |
| `GET /purchase/report/supplierTransactions` | `receiptAmount` |
| `GET /purchase/report/supplierTransactionsDetails` | `receiptAmount` |
> æ˜Žç»†æŽ¥å£çš„ `paymentAmount` / `receiptAmount` æ˜¯**往来方维度**的合计(该客户/供应商全部流水合计),流水本身不挂到单张合同上,因此明细各行该列值相同。若前端希望只在列表页展示,明细页可隐藏该列。
### 3.3 æ–°å¢žï¼šä»˜æ¬¾å›žæ¬¾æµæ°´å››æŽ¥å£
前缀 `/paymentRecord`。
| æŽ¥å£ | æ–¹æ³• | è¯´æ˜Ž |
| --- | --- | --- |
| `/paymentRecord/listPage` | GET | åˆ†é¡µæŸ¥è¯¢ï¼Œæ”¯æŒ `partyType`、`partyId`、`direction`、`paymentMethod` |
| `/paymentRecord/add` | POST | æ–°å¢ž |
| `/paymentRecord/update` | POST | ä¿®æ”¹ï¼Œå¿…须带 `id` |
| `/paymentRecord/delete` | DELETE | æ‰¹é‡åˆ é™¤ï¼Œbody ä¸º id æ•°ç»„ |
请求体字段:
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
| --- | --- | --- | --- |
| `id` | Long | ä¿®æ”¹å¿…å¡« | ä¸»é”® |
| `partyType` | Integer | æ˜¯ | `1` å®¢æˆ·ï¼Œ`2` ä¾›åº”商 |
| `partyId` | Long | æ˜¯ | å®¢æˆ· id æˆ–供应商 id |
| `direction` | Integer | æ˜¯ | `1` ä»˜æ¬¾ï¼Œ`2` å›žæ¬¾ |
| `amount` | BigDecimal | æ˜¯ | é‡‘额 |
| `recordDate` | String | å¦ | å‘生日期,格式 `yyyy-MM-dd` |
| `paymentMethod` | String | å¦ | ä»˜æ¬¾/回款方式 |
| `remark` | String | å¦ | å¤‡æ³¨ |
`tenantId` / `deptId` / `createUser` / `createTime` / `updateUser` / `updateTime` ç”±åŽç«¯è‡ªåŠ¨å¡«å……ï¼Œ**前端不要传**。
**查询参数示例**(客户往来页「新增付款」后查看该客户流水):
```
GET /paymentRecord/listPage?pageNum=1&pageSize=10&partyType=1&partyId=449&direction=1
```
`listPage` è¿”回结构为 MyBatis-Plus åˆ†é¡µå¯¹è±¡ï¼Œå‰ç«¯å– `data.records`。
---
## 4. éœ€æ±‚ 15:备件关联产品与出入库流水
### 4.1 å¤‡ä»¶æ–°å¢žä¸¤ä¸ªå­—段
`GET /spareParts/listPage`、`POST /spareParts/add`、`POST /spareParts/update` çš„备件对象**新增**:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- |
| `productId` | Long | å…³è”产品大类 id |
| `productModelId` | Long | å…³è”产品**规格型号** id |
**这是打通采购的业务钩子**:采购台账新增明细时,后端用明细的 `productModelId` åŽ»åŒ¹é… `spare_parts.product_model_id`。匹配上则:
1. `spare_parts.quantity` ç´¯åŠ è¯¥æ˜Žç»†æ•°é‡ï¼›
2. å†™ä¸€æ¡å¤‡ä»¶å…¥åº“流水(`direction=1`、`sourceType=1`、`sourceId=采购台账id`、`sourceNo=采购合同号`)。
未维护 `productModelId` çš„备件**不会**被匹配,静默跳过,不影响采购台账保存。
> ä»…在采购台账**新增明细**时回写;编辑采购台账走更新分支,不会重复计数。
### 4.2 æ–°å¢žï¼šå¤‡ä»¶å‡ºå…¥åº“流水
前缀 `/sparePartsRecord`。
| æŽ¥å£ | æ–¹æ³• | è¯´æ˜Ž |
| --- | --- | --- |
| `/sparePartsRecord/listPage` | GET | åˆ†é¡µæŸ¥è¯¢ï¼Œæ”¯æŒ `sparePartsId`、`direction`、`sourceType` |
| `/sparePartsRecord/delete` | DELETE | æ‰¹é‡åˆ é™¤ï¼Œbody ä¸º id æ•°ç»„ |
返回字段:
| å­—段 | è¯´æ˜Ž |
| --- | --- |
| `sparePartsId` | å¤‡ä»¶ id |
| `sparePartsName` | å¤‡ä»¶åç§°ï¼ˆåŽç«¯ join å¸¦å‡ºï¼‰ |
| `direction` | `1` å…¥åº“,`2` å‡ºåº“ |
| `quantity` | æ•°é‡ |
| `sourceType` | æ¥æºç±»åž‹ï¼Œè§ä¸‹è¡¨ |
| `sourceId` | æ¥æºå•据 id |
| `sourceNo` | æ¥æºå•号 |
| `remark` | å¤‡æ³¨ |
| `createTime` | å‘生时间 |
`sourceType` æžšä¸¾ï¼š
| å€¼ | å«ä¹‰ | è§¦å‘点 |
| --- | --- | --- |
| `1` | é‡‡è´­å…¥åº“ | é‡‡è´­å°è´¦æ–°å¢žå«è¯¥è§„格的明细 |
| `2` | ç»´ä¿®é¢†ç”¨ | è®¾å¤‡æŠ¥ä¿®å•更新时领用备件 |
| `3` | ä¿å…»é¢†ç”¨ | è®¾å¤‡ä¿å…»å•更新时领用备件 |
> ç»´ä¿®/保养**领用**仍沿用原有 `spare_parts_requisition_record`(设备维度,另有页面);本次新增的 `spare_parts_record` æ˜¯**备件维度**的进出流水,两者并存,职责不同。
### 4.3 å‰ç«¯é¡µé¢å»ºè®®
- å¤‡ä»¶ç»´æŠ¤è¡¨å•:产品大类、规格型号两个下拉(复用产品接口)。
- æ–°å¢žé¡µé¢ï¼šå¤‡ä»¶å‡ºå…¥åº“流水(放在设备管理或备件管理下),列表列建议:备件名称、方向、数量、来源类型、来源单号、时间。
---
## 5. éœ€æ±‚ 14:新增入库关联订单反显
### 5.1 åŽç«¯ä¿®å¤è¯´æ˜Žï¼ˆå‰ç«¯æŽ¥å£å½¢æ€ä¸å˜ï¼‰
原 `listProcurementBySalesLedgerId` çš„ join æ¡ä»¶å†™äº† `slp.type = 2`,但 `slp` æœªå®šä¹‰ï¼Œå¯¼è‡´**类型过滤实际不生效**,可能返回非采购明细。已修正为 `t2.type = 2`。
### 5.2 æ–°å¢žå¯é€‰å‚æ•°
`GET /stockin/productlist` æ–°å¢žå¯é€‰å‚数:
| å‚æ•° | ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- |
| `purchaseLedgerId` | Integer | **推荐**。按采购台账 id ç²¾ç¡®åæ˜¾æ˜Žç»† |
| `purchaseContractNumber` | String | å…¼å®¹ä¿ç•™ï¼ŒæŒ‰åˆåŒå·åŒ¹é… |
两个参数可只传其一;`purchaseLedgerId` ä¼˜å…ˆã€‚前端从采购订单下拉选中订单后,直接把该订单的 id ä½œä¸º `purchaseLedgerId` ä¼ å…¥ï¼Œæ¯”传合同号更可靠(合同号可能重复/为空)。
出库侧反显沿用已有接口,**无变更**:`GET /sales/ledger/getSalesLedgerWithProducts`。
---
## 6. è”调自查清单
- [ ] å‡ºåº“表单:选规格后能拉到批次列表,批次为必填项
- [ ] å‡ºåº“(type=2)后,出库台账「发货单号」列有值,格式 `SH+日期+流水`
- [ ] å‘货台账中能看到该自动生成的单,状态为 `已发货`
- [ ] åŒä¸€äº§å“å†æ¬¡å‡ºåº“,生成**另一张**发货单(不是被拦截)
- [ ] åˆ é™¤å‡ºåº“单后,发货台账、发货明细、库存扣减记录一并消失
- [ ] å®¢æˆ·å¾€æ¥åˆ—表/明细出现「付款」列,插入一条 `direction=1` æµæ°´åŽæ•°å€¼å˜åŒ–,且「应收」不变
- [ ] ä¾›åº”商往来列表/明细出现「回款」列,逻辑同上
- [ ] å¤‡ä»¶ç»´æŠ¤èƒ½ä¿å­˜äº§å“å¤§ç±» + è§„格型号
- [ ] æ–°å¢žä¸€å¼ å«è¯¥è§„格的采购台账,备件数量增加,备件流水出现 `direction=1` è®°å½•
- [ ] è®¾å¤‡æŠ¥ä¿®/保养领用备件后,备件流水出现 `direction=2` è®°å½•
- [ ] æ–°å¢žå…¥åº“选订单后,明细只显示采购类(type=2)产品
doc/20260920_²É¹ºÉêÇëÄ£¿éǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,322 @@
# é‡‡è´­ç”³è¯·æ¨¡å—前端联调文档
更新时间:2026-09-20
适用版本:本次后端变更后
## 1. å˜æ›´èŒƒå›´
1. èœå•「采购管理 â†’ **采购审核**」改名为「**采购管理 â†’ é‡‡è´­ç”³è¯·**」,去掉审核环节。
2. æ–°å¢žé‡‡è´­ç”³è¯·å•模块(独立于采购台账),支持保存草稿、编辑、删除、**转订单**。
3. è½¬è®¢å•复用采购台账新增逻辑:自动生成采购合同号、写入采购台账与产品明细,并**自动提交 OA é‡‡è´­å®¡æ‰¹**(审批流程不在本模块内,仍在「审批管理」中处理)。
4. åŽç«¯æŽ¥å£å‰ç¼€ï¼š`/purchase/application`。
### 1.1 èœå•改动(数据库已执行)
| é¡¹ | åŽŸå€¼ | æ–°å€¼ |
| --- | --- | --- |
| `sys_menu.menu_name`(menu_id=2355) | é‡‡è´­å®¡æ ¸ | é‡‡è´­ç”³è¯· |
| `path` | purchaseReview | purchaseApplication |
| `component` | procurementManagement/purchaseReview/index | procurementManagement/purchaseApplication/index |
**前端需新增页面**:`src/views/procurementManagement/purchaseApplication/index.vue`(原 `purchaseReview` é¡µé¢å·²ä¸å†è¢«èœå•引用,可删除或保留)。
---
## 2. çŠ¶æ€æžšä¸¾
| å€¼ | å«ä¹‰ | å‰ç«¯å¯æ“ä½œé¡¹ |
| --- | --- | --- |
| `0` | è‰ç¨¿ | ç¼–辑、删除、转订单 |
| `1` | å·²ç”Ÿæˆè®¢å• | ä»…查看 |
说明:
- æ–°å¢žçš„申请单**强制为草稿**,前端不需要也不允许传 `status`。
- ç¼–辑、删除接口都会校验状态,非草稿会返回错误(见 4.6 å¸¸è§é”™è¯¯ï¼‰ã€‚
- è½¬è®¢å•成功后状态自动变为 `1`,并写入 `purchaseLedgerId`、`convertTime`。
---
## 3. å…¬å…±å­—段说明(后端自动维护,前端只读)
| å­—段 | è¯´æ˜Ž |
| --- | --- |
| `applicationNo` | ç”³è¯·å•号,新增时后端生成,格式 `CGSQ` + æ—¥æœŸ + å½“日流水,如 `CGSQ20260920001`。**前端不要传**,传了也会被忽略。 |
| `supplierName` | ä¾æ® `supplierId` åæŸ¥å›žå¡«ï¼Œå‰ç«¯ä¸å¿…传(传了以后端结果为准)。 |
| `applicantId` / `applicantName` | ä¸ä¼ æ—¶é»˜è®¤å–当前登录用户;也可显式传 `applicantId` æŒ‡å®šç”³è¯·äººã€‚`applicantName` ç”±åŽç«¯æ ¹æ® `applicantId` æŸ¥æ˜µç§°å›žå¡«ã€‚ |
| `applyDate` | ä¸ä¼ æ—¶é»˜è®¤å½“天。 |
| `productCategory` / `specificationModel` | **明细行**中的这两个字段由后端根据 `productId` / `productModelId` åæŸ¥å†™å…¥ï¼Œå‰ç«¯åªéœ€ä¼  id,传了也会被覆盖。 |
| `tenantId` / `deptId` / `createUser` / `createTime` / `updateTime` | åŽç«¯è‡ªåŠ¨å¡«å……ï¼Œå‰ç«¯ä¸è¦ä¼ ã€‚ |
| `purchaseLedgerId` / `convertTime` | è½¬è®¢å•成功后才有值。 |
---
## 4. æŽ¥å£æ¸…单
### 4.1 æŸ¥è¯¢é‡‡è´­ç”³è¯·åˆ—表
`GET /purchase/application/list`
**请求参数**(均为可选,query å½¢å¼ï¼‰
| å‚æ•° | ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- |
| `applicationNo` | string | ç”³è¯·å•号,模糊匹配 |
| `projectName` | string | é¡¹ç›®åç§°ï¼Œæ¨¡ç³ŠåŒ¹é… |
| `supplierId` | number | ä¾›åº”商id,精确匹配 |
| `status` | number | çŠ¶æ€ï¼Œ`0` è‰ç¨¿ / `1` å·²ç”Ÿæˆè®¢å• |
| `startDate` | string | ç”³è¯·æ—¥æœŸèµ·ï¼Œ`yyyy-MM-dd` |
| `endDate` | string | ç”³è¯·æ—¥æœŸæ­¢ï¼Œ`yyyy-MM-dd` |
| `pageNum` | number | é¡µç ï¼Œåˆ†é¡µå‚æ•° |
| `pageSize` | number | æ¯é¡µæ¡æ•°ï¼Œåˆ†é¡µå‚æ•° |
排序:按 `id` å€’序,**不支持传入排序字段**。
**返回**:RuoYi æ ‡å‡†åˆ†é¡µç»“æž„ `{ total, rows, code, msg }`(注意是 `rows`,不是 `records`)。
```json
{
  "total": 1,
  "rows": [
    {
      "id": 3,
      "applicationNo": "CGSQ20260920001",
      "supplierId": 12,
      "supplierName": "XX供应商",
      "projectName": "南通XX项目",
      "salesContractNo": "XS20260901001",
      "salesLedgerId": 88,
      "applicantId": 1,
      "applicantName": "张三",
      "applyDate": "2026-09-20",
      "expectedArrivalDate": "2026-09-30",
      "contractAmount": 11300.00,
      "paymentMethod": "电汇",
      "remarks": "紧急",
      "status": 0,
      "purchaseLedgerId": null,
      "convertTime": null,
      "createTime": "2026-09-20 10:12:33",
      "updateTime": "2026-09-20 10:12:33"
    }
  ],
  "code": 200,
  "msg": "查询成功"
}
```
> åˆ—表接口**不返回** `productData` æ˜Žç»†ï¼Œæ˜Žç»†è¯·å•独调 4.2 èŽ·å–ã€‚
---
### 4.2 æŸ¥è¯¢é‡‡è´­ç”³è¯·è¯¦æƒ…
`GET /purchase/application/getById?id={id}`
**返回**:AjaxResult,`data` ä¸ºç”³è¯·å•对象(字段同 4.1,另含 `productData` æ˜Žç»†æ•°ç»„)。
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "id": 3,
    "applicationNo": "CGSQ20260920001",
    "supplierId": 12,
    "supplierName": "XX供应商",
    "projectName": "南通XX项目",
    "applyDate": "2026-09-20",
    "contractAmount": 11300.00,
    "status": 0,
    "productData": [
      {
        "id": 21,
        "purchaseApplicationId": 3,
        "productId": 5,
        "productModelId": 17,
        "productCategory": "铜材",
        "specificationModel": "T2-30x3",
        "unit": "ç±³",
        "quantity": 100,
        "taxRate": 13,
        "taxInclusiveUnitPrice": 113.00,
        "taxInclusiveTotalPrice": 11300.00,
        "taxExclusiveTotalPrice": 10000.00,
        "invoiceType": "增值税专用发票",
        "isChecked": true,
        "remark": ""
      }
    ]
  }
}
```
申请单不存在时返回 `code=500`、`msg="采购申请不存在"`。
---
### 4.3 æ–°å¢žé‡‡è´­ç”³è¯·ï¼ˆè‰ç¨¿ï¼‰
`POST /purchase/application/add`
`Content-Type: application/json`
**请求体**:同 4.2 çš„ `data` ç»“构,`id`、`applicationNo`、`status`、`purchaseLedgerId`、`convertTime` ä¸éœ€è¦ä¼ ã€‚
**返回**:`data` ä¸ºæ–°ç”³è¯·å• id。
```json
{ "code": 200, "msg": "操作成功", "data": 3 }
```
---
### 4.4 ä¿®æ”¹é‡‡è´­ç”³è¯·
`POST /purchase/application/edit`
`Content-Type: application/json`
**请求体**:同 4.3,**必须带 `id`**,明细需全量提交(后端为「先删后插」)。
**返回**:`{ "code": 200, "msg": "操作成功" }`
> ä»…草稿状态可修改。已生成订单的申请单修改会返回错误。
---
### 4.5 åˆ é™¤é‡‡è´­ç”³è¯·
`DELETE /purchase/application/delete`
`Content-Type: application/json`
**请求体**:id æ•°ç»„,支持批量。
```json
[3, 4]
```
**返回**:`{ "code": 200, "msg": "操作成功" }`
> ä»…草稿状态可删除。请求体为空时返回 `code=500`、`msg="请选择要删除的采购申请"`。
---
### 4.6 é‡‡è´­ç”³è¯·è½¬è®¢å•
`POST /purchase/application/convertToOrder?id={id}`
> æ³¨æ„ï¼š`id` æ˜¯ **query å‚æ•°**,不是请求体。
**返回**:`data` ä¸ºç”Ÿæˆçš„采购台账 id(即「采购订单」id,后续入库、质检、付款等流程都用它)。
```json
{ "code": 200, "msg": "操作成功", "data": 1201 }
```
**转订单做了什么**(前端了解即可,无需额外调用):
1. æ ¡éªŒï¼šä¾›åº”商已选择且存在、存在产品明细;
2. æŠ¢å çŠ¶æ€ï¼šä»¥æ¡ä»¶æ›´æ–°æŠŠçŠ¶æ€ä»Ž `0` æ”¹ä¸º `1`,**并发重复点击只有第一次会成功**;
3. ç”Ÿæˆé‡‡è´­åˆåŒå·ï¼ˆç”±é‡‡è´­å°è´¦ä¾§è§„则生成);
4. å†™å…¥ `purchase_ledger` ä¸»è¡¨ + `sales_ledger_product` äº§å“æ˜Žç»†ï¼ˆ`type=2`);
5. è‡ªåŠ¨æäº¤ OA é‡‡è´­å®¡æ‰¹ï¼ˆæµç¨‹è§ã€Œå®¡æ‰¹ç®¡ç†ã€ï¼Œæœ¬æ¨¡å—不再有审核按钮);
6. å›žå¡« `purchaseLedgerId`、`convertTime`。
任一步失败整个事务回滚,申请单仍保持草稿状态。
---
## 5. å¸¸è§é”™è¯¯è¿”回
统一为 `{ "code": 500, "msg": "<错误信息>" }`,前端直接 toast `msg` å³å¯ã€‚
| msg | è§¦å‘场景 |
| --- | --- |
| `采购申请不存在` | è¯¦æƒ…/编辑/转订单传入的 id ä¸å­˜åœ¨ |
| `采购申请id不能为空` | ç¼–辑、转订单未传 id |
| `已生成订单的采购申请不允许修改` | å¯¹ `status=1` çš„单据调用编辑 |
| `已生成订单的采购申请不允许删除:<单号>` | å¯¹ `status=1` çš„单据调用删除,会回显具体单号 |
| `请选择要删除的采购申请` | åˆ é™¤æŽ¥å£è¯·æ±‚体为空数组 |
| `请先选择供应商再转订单` | è½¬è®¢å•æ—¶ `supplierId` ä¸ºç©º |
| `供应商不存在,请重新选择` | `supplierId` åœ¨ä¾›åº”商表中查不到 |
| `采购申请没有产品明细,无法转订单` | ç”³è¯·å•下没有明细行 |
| `该采购申请已生成订单,请勿重复转换` | å¹¶å‘/重复点击转订单 |
| `转订单失败:<原因>` | é‡‡è´­å°è´¦ä¾§å¤±è´¥ï¼Œå¦‚ `转订单失败:请先配置采购审批模板` |
---
## 6. è¯·æ±‚示例(curl)
```bash
# åˆ—表
curl -X GET "http://localhost:8080/purchase/application/list?status=0&pageNum=1&pageSize=10" \
  -H "Authorization: Bearer <token>"
# è¯¦æƒ…
curl -X GET "http://localhost:8080/purchase/application/getById?id=3" \
  -H "Authorization: Bearer <token>"
# æ–°å¢ž
curl -X POST "http://localhost:8080/purchase/application/add" \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{
    "supplierId": 12,
    "projectName": "南通XX项目",
    "salesContractNo": "XS20260901001",
    "salesLedgerId": 88,
    "expectedArrivalDate": "2026-09-30",
    "contractAmount": 11300.00,
    "paymentMethod": "电汇",
    "remarks": "紧急",
    "productData": [
      {
        "productId": 5,
        "productModelId": 17,
        "unit": "ç±³",
        "quantity": 100,
        "taxRate": 13,
        "taxInclusiveUnitPrice": 113.00,
        "taxInclusiveTotalPrice": 11300.00,
        "taxExclusiveTotalPrice": 10000.00,
        "invoiceType": "增值税专用发票",
        "isChecked": true
      }
    ]
  }'
# è½¬è®¢å•
curl -X POST "http://localhost:8080/purchase/application/convertToOrder?id=3" \
  -H "Authorization: Bearer <token>"
```
---
## 7. å‰ç«¯æ”¹é€ å»ºè®®
1. æ–°å»º `procurementManagement/purchaseApplication/index.vue`,页面结构可直接沿用原采购审核页面,**去掉审核相关按钮和审批状态列**。
2. åˆ—表页新增「状态」列,`0` æ˜¾ç¤ºã€Œè‰ç¨¿ã€ï¼Œ`1` æ˜¾ç¤ºã€Œå·²ç”Ÿæˆè®¢å•」,建议用 tag åŒºåˆ†é¢œè‰²ã€‚
3. åˆ—表页新增「状态」筛选项,值 `0` / `1`;新增「申请日期」区间筛选项,对应 `startDate` / `endDate`。
4. ã€Œç¼–辑」「删除」「转订单」三个按钮仅在 `status === 0` æ—¶æ˜¾ç¤ºï¼Œè½¬è®¢å•建议加二次确认。
5. è½¬è®¢å•成功后用返回的 `data`(采购台账id)跳转采购台账详情,并刷新列表。
6. æ–°å¢ž/编辑表单中的供应商、销售合同号建议复用采购台账的现有下拉/选择逻辑(`/purchase/ledger/getSalesNo`、`/purchase/ledger/getPurchaseNo` ç­‰ï¼‰ã€‚
7. æ˜Žç»†è¡Œçš„ `productCategory`(产品大类)、`specificationModel`(规格型号)为后端回填字段,表单里只做展示,提交时传 `productId` / `productModelId` å³å¯ã€‚
---
## 8. è”调检查清单
1. èœå•展示为「采购申请」,点击可进入新页面,无 404。
2. `/list` ä¸ä¼ ä»»ä½•条件:返回全部数据,`rows` å« `status`、`applicationNo`、`supplierName` ç­‰å­—段。
3. `/list?status=0`:只返回草稿;`/list?status=1`:只返回已生成订单。
4. `/list?startDate=2026-09-01&endDate=2026-09-30`:按申请日期过滤。
5. `/add` åªä¼  `supplierId` + `productData`:能新增成功,返回 id,列表能看到草稿,`applicationNo` è‡ªåŠ¨ç”Ÿæˆã€‚
6. `/getById`:返回含 `productData` çš„完整明细,`productCategory` / `specificationModel` å·²ç”±åŽç«¯å›žå¡«ã€‚
7. `/edit` ä¿®æ”¹è‰ç¨¿ï¼šæ˜Žç»†å…¨é‡æ›¿æ¢ç”Ÿæ•ˆï¼ˆæ”¹æ•°é‡ã€å¢žåˆ è¡Œéƒ½æ­£ç¡®ï¼‰ã€‚
8. `/edit` ä¿®æ”¹ `status=1` çš„单据:返回「已生成订单的采购申请不允许修改」。
9. `/delete` åˆ é™¤è‰ç¨¿ï¼šæˆåŠŸï¼›`/delete` åˆ é™¤ `status=1`:返回带单号的错误提示。
10. `/convertToOrder`:成功后状态变 `1`,返回采购台账id;**打开采购台账列表能查到该采购合同号**。
11. `/convertToOrder` å¯¹åŒä¸€ id å†ç‚¹ä¸€æ¬¡ï¼šè¿”回「该采购申请已生成订单,请勿重复转换」,且**不会**生成第二条采购台账。
12. è½¬è®¢å•后去「审批管理」:能看到一条采购审批(标题为「采购合同号 + å®¡æ‰¹ã€ï¼‰å¾…处理。
13. æ—  `supplierId` æ—¶ç‚¹è½¬è®¢å•:返回「请先选择供应商再转订单」;无明细时返回「采购申请没有产品明细,无法转订单」。
sql/DbTool.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,193 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
 * æ•°æ®åº“直连小工具。用法见 printUsage()。
 * è¿è¡Œï¼šbash sql/db.sh "SELECT 1"
 */
public class DbTool {
    private static final String DEFAULT_URL =
            "jdbc:mysql://localhost:3306/product-inventory-management-rbhb"
                    + "?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull"
                    + "&serverTimezone=GMT%2B8&sslMode=DISABLED&allowPublicKeyRetrieval=true";
    private static final String DEFAULT_USER = "root";
    private static final String DEFAULT_PASS = "123456";
    public static void main(String[] args) throws Exception {
        String url = DEFAULT_URL;
        String user = DEFAULT_USER;
        String pass = DEFAULT_PASS;
        String sqlText = null;
        Path scriptFile = null;
        List<String> positional = new ArrayList<>();
        for (int i = 0; i < args.length; i++) {
            String arg = args[i];
            switch (arg) {
                case "-url" -> url = args[++i];
                case "-user" -> user = args[++i];
                case "-pass" -> pass = args[++i];
                case "-f" -> scriptFile = Path.of(args[++i]);
                case "-h", "--help" -> {
                    printUsage();
                    return;
                }
                default -> positional.add(arg);
            }
        }
        if (scriptFile != null) {
            sqlText = Files.readString(scriptFile, StandardCharsets.UTF_8);
        } else if (!positional.isEmpty()) {
            sqlText = String.join(" ", positional);
        } else if (System.console() != null) {
            printUsage();
            return;
        } else {
            Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8).useDelimiter("\\A");
            sqlText = scanner.hasNext() ? scanner.next() : null;
        }
        if (sqlText == null || sqlText.isBlank()) {
            printUsage();
            return;
        }
        List<String> statements = splitStatements(sqlText);
        if (statements.isEmpty()) {
            printUsage();
            return;
        }
        try (Connection conn = DriverManager.getConnection(url, user, pass)) {
            for (String sql : statements) {
                runOne(conn, sql);
            }
        }
    }
    private static void runOne(Connection conn, String sql) throws Exception {
        long start = System.currentTimeMillis();
        try (Statement stmt = conn.createStatement()) {
            boolean hasResultSet = stmt.execute(sql);
            long cost = System.currentTimeMillis() - start;
            if (hasResultSet) {
                try (ResultSet rs = stmt.getResultSet()) {
                    printResultSet(rs);
                }
                System.out.println("耗时 " + cost + " ms");
            } else {
                System.out.println("受影响行数: " + stmt.getUpdateCount() + "  è€—æ—¶ " + cost + " ms");
            }
        } catch (Exception e) {
            System.out.println("执行失败: " + e.getMessage());
            System.out.println("SQL: " + sql);
            throw e;
        }
    }
    private static void printResultSet(ResultSet rs) throws Exception {
        ResultSetMetaData meta = rs.getMetaData();
        int columnCount = meta.getColumnCount();
        String[] headers = new String[columnCount];
        int[] widths = new int[columnCount];
        for (int i = 0; i < columnCount; i++) {
            headers[i] = meta.getColumnLabel(i + 1);
            widths[i] = displayWidth(headers[i]);
        }
        List<String[]> rows = new ArrayList<>();
        while (rs.next()) {
            String[] row = new String[columnCount];
            for (int i = 0; i < columnCount; i++) {
                Object value = rs.getObject(i + 1);
                row[i] = value == null ? "NULL" : String.valueOf(value);
                widths[i] = Math.max(widths[i], displayWidth(row[i]));
            }
            rows.add(row);
        }
        StringBuilder separator = new StringBuilder();
        for (int width : widths) {
            separator.append('-').append("-".repeat(width)).append('-');
        }
        System.out.println(separator);
        System.out.println(joinRow(headers, widths));
        System.out.println(separator);
        for (String[] row : rows) {
            System.out.println(joinRow(row, widths));
        }
        System.out.println(separator);
        System.out.println("共 " + rows.size() + " è¡Œ");
    }
    private static String joinRow(String[] cells, int[] widths) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < cells.length; i++) {
            sb.append("| ").append(cells[i]);
            sb.append(" ".repeat(Math.max(0, widths[i] - displayWidth(cells[i]) + 1)));
        }
        return sb.append('|').toString();
    }
    /**
     * å…¨è§’字符占两个终端宽度,按 2 è®¡ç®—以保证表格对齐。
     */
    private static int displayWidth(String text) {
        int width = 0;
        for (int i = 0; i < text.length(); i++) {
            width += text.charAt(i) > 0x2E80 ? 2 : 1;
        }
        return width;
    }
    /**
     * æŒ‰åˆ†å·åˆ‡åˆ†è¯­å¥ï¼Œå¿½ç•¥ç©ºè¯­å¥ä¸Žçº¯æ³¨é‡Šè¡Œã€‚
     * ç®€å•实现:不支持字符串字面量中包含分号的极端场景。
     */
    private static List<String> splitStatements(String sqlText) {
        List<String> statements = new ArrayList<>();
        for (String part : sqlText.split(";")) {
            String trimmed = part.trim();
            if (trimmed.isEmpty() || isCommentOnly(trimmed)) {
                continue;
            }
            statements.add(trimmed);
        }
        return statements;
    }
    private static boolean isCommentOnly(String sql) {
        for (String line : sql.split("\\R")) {
            String stripped = line.trim();
            if (!stripped.isEmpty() && !stripped.startsWith("--") && !stripped.startsWith("#")) {
                return false;
            }
        }
        return true;
    }
    private static void printUsage() {
        System.out.println("""
                æ•°æ®åº“直连工具
                ç”¨æ³•:
                  bash sql/db.sh "SQL语句"
                  bash sql/db.sh -f è„šæœ¬.sql
                  echo "SQL语句" | bash sql/db.sh
                å¯è¦†ç›–连接参数:
                  -url jdbc:mysql://host:3306/db?...   -user root   -pass 123456
                """);
    }
}
sql/db.sh
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# æ•°æ®åº“直连工具包装脚本,自动定位 MySQL JDBC é©±åŠ¨å¹¶è°ƒç”¨ sql/DbTool.java
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
JAR="$(ls "$HOME"/.m2/repository/com/mysql/mysql-connector-j/*/mysql-connector-j-*.jar 2>/dev/null | grep -v sources | tail -1 || true)"
if [ -z "$JAR" ]; then
  JAR="$(ls "$HOME"/.m2/repository/mysql/mysql-connector-java/*/mysql-connector-java-*.jar 2>/dev/null | grep -v sources | tail -1 || true)"
fi
if [ -z "$JAR" ]; then
  echo "未找到 MySQL JDBC é©±åŠ¨ï¼Œè¯·ç¡®è®¤ ~/.m2 ä¸‹å­˜åœ¨ mysql-connector-j" >&2
  exit 1
fi
if command -v cygpath >/dev/null 2>&1; then
  JAR="$(cygpath -m "$JAR")"
  SOURCE="$(cygpath -m "$SCRIPT_DIR/DbTool.java")"
else
  SOURCE="$SCRIPT_DIR/DbTool.java"
fi
exec java -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 --class-path "$JAR" "$SOURCE" "$@"
src/main/java/com/ruoyi/account/controller/PaymentRecordController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.pojo.PaymentRecord;
import com.ruoyi.account.service.PaymentRecordService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * å®¢æˆ·/供应商往来付款回款流水
 *
 * @author :yys
 */
@RestController
@Tag(name = "往来付款回款")
@RequestMapping("/paymentRecord")
@AllArgsConstructor
public class PaymentRecordController extends BaseController {
    private PaymentRecordService paymentRecordService;
    @GetMapping("/listPage")
    @Log(title = "往来付款回款-分页查询", businessType = BusinessType.OTHER)
    public AjaxResult listPage(Page page, PaymentRecord paymentRecord) {
        IPage<PaymentRecord> listPage = paymentRecordService.listPage(page, paymentRecord);
        return AjaxResult.success(listPage);
    }
    @PostMapping("/add")
    @Log(title = "往来付款回款-新增", businessType = BusinessType.INSERT)
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult add(@RequestBody PaymentRecord paymentRecord) {
        validate(paymentRecord);
        boolean save = paymentRecordService.save(paymentRecord);
        return save ? AjaxResult.success() : AjaxResult.error();
    }
    @PostMapping("/update")
    @Log(title = "往来付款回款-修改", businessType = BusinessType.UPDATE)
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult update(@RequestBody PaymentRecord paymentRecord) {
        if (paymentRecord.getId() == null) {
            return AjaxResult.error("缺少主键id");
        }
        validate(paymentRecord);
        boolean update = paymentRecordService.updateById(paymentRecord);
        return update ? AjaxResult.success() : AjaxResult.error();
    }
    @DeleteMapping("/delete")
    @Log(title = "往来付款回款-删除", businessType = BusinessType.DELETE)
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return AjaxResult.error("请选择至少一条数据");
        }
        boolean delete = paymentRecordService.removeBatchByIds(ids);
        return delete ? AjaxResult.success() : AjaxResult.error();
    }
    private void validate(PaymentRecord paymentRecord) {
        if (paymentRecord.getPartyType() == null || paymentRecord.getPartyId() == null) {
            throw new RuntimeException("请选择往来方");
        }
        if (paymentRecord.getDirection() == null) {
            throw new RuntimeException("请选择方向(付款/回款)");
        }
        if (paymentRecord.getAmount() == null) {
            throw new RuntimeException("请填写金额");
        }
    }
}
src/main/java/com/ruoyi/account/mapper/PaymentRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
package com.ruoyi.account.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.PaymentRecord;
/**
 * @author :yys
 */
public interface PaymentRecordMapper extends BaseMapper<PaymentRecord> {
}
src/main/java/com/ruoyi/account/pojo/PaymentRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
package com.ruoyi.account.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å®¢æˆ·/供应商往来付款回款流水
 *
 * @author :yys
 */
@Data
@TableName("payment_record")
@Schema(name = "PaymentRecord", description = "往来付款回款流水")
public class PaymentRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    @Schema(description = "主键id")
    private Long id;
    @Schema(description = "往来方类型 1客户 2供应商")
    private Integer partyType;
    @Schema(description = "客户id或供应商id")
    private Long partyId;
    @Schema(description = "方向 1付款 2回款")
    private Integer direction;
    @Schema(description = "金额")
    private BigDecimal amount;
    @Schema(description = "发生日期")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate recordDate;
    @Schema(description = "付款/回款方式")
    private String paymentMethod;
    @Schema(description = "备注")
    private String remark;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
src/main/java/com/ruoyi/account/service/PaymentRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.account.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.account.pojo.PaymentRecord;
/**
 * @author :yys
 */
public interface PaymentRecordService extends IService<PaymentRecord> {
    IPage<PaymentRecord> listPage(Page page, PaymentRecord req);
}
src/main/java/com/ruoyi/account/service/impl/PaymentRecordServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.mapper.PaymentRecordMapper;
import com.ruoyi.account.pojo.PaymentRecord;
import com.ruoyi.account.service.PaymentRecordService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
/**
 * @author :yys
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class PaymentRecordServiceImpl extends ServiceImpl<PaymentRecordMapper, PaymentRecord> implements PaymentRecordService {
    private final PaymentRecordMapper paymentRecordMapper;
    @Override
    public IPage<PaymentRecord> listPage(Page page, PaymentRecord req) {
        LambdaQueryWrapper<PaymentRecord> wrapper = new LambdaQueryWrapper<PaymentRecord>()
                .eq(req.getPartyType() != null, PaymentRecord::getPartyType, req.getPartyType())
                .eq(req.getPartyId() != null, PaymentRecord::getPartyId, req.getPartyId())
                .eq(req.getDirection() != null, PaymentRecord::getDirection, req.getDirection())
                .like(StringUtils.isNotBlank(req.getPaymentMethod()), PaymentRecord::getPaymentMethod, req.getPaymentMethod())
                .orderByDesc(PaymentRecord::getRecordDate)
                .orderByDesc(PaymentRecord::getId);
        return paymentRecordMapper.selectPage(page, wrapper);
    }
}
src/main/java/com/ruoyi/basic/pojo/OilDepot.java
@@ -70,12 +70,13 @@
    /**
     * æ›´æ–°æ—¶é—´
     */
    @TableField(fill = FieldFill.INSERT)
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * æ›´æ–°äºº
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
src/main/java/com/ruoyi/basic/pojo/TankInfo.java
@@ -73,12 +73,13 @@
    /**
     * æ›´æ–°æ—¶é—´
     */
    @TableField(fill = FieldFill.INSERT)
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * æ›´æ–°äºº
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
src/main/java/com/ruoyi/collaborativeApproval/controller/RulesRegulationsManagementController.java
@@ -7,6 +7,7 @@
import com.ruoyi.collaborativeApproval.pojo.RulesRegulationsManagement;
import com.ruoyi.collaborativeApproval.pojo.SealApplicationManagement;
import com.ruoyi.collaborativeApproval.service.RulesRegulationsManagementService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -17,6 +18,7 @@
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.List;
@RestController
@@ -43,6 +45,9 @@
    @PostMapping("/update")
    @Operation(summary = "修改")
    public AjaxResult update(@RequestBody RulesRegulationsManagement rulesRegulationsManagement){
        // å‰ç«¯å›žä¼ æ•´è¡Œä¼šå¸¦ä¸Šæ—§çš„ updateTime/updateUser,导致自动填充被跳过,需显式同步为当前操作人与时间
        rulesRegulationsManagement.setUpdateTime(LocalDateTime.now());
        rulesRegulationsManagement.setUpdateUser(SecurityUtils.getUserId().intValue());
        return AjaxResult.success(rulesRegulationsManagementService.updateById(rulesRegulationsManagement));
    }
src/main/java/com/ruoyi/collaborativeApproval/service/impl/MeetingServiceImpl.java
@@ -173,28 +173,7 @@
        //参会人员id(人员台账和用户表通过人员编号和用户名称(登录账号)做匹配)
        if (Integer.valueOf(1).equals(meetApplication.getPublishStatus())) {
            MeetApplication application = meetApplicationMapper.selectById(meetApplication.getId());
            String cleanStr = application.getParticipants()
                    .replaceAll("^\\[|\\]$", "")
                    .trim();
            List<Long> userIds = Arrays.stream(cleanStr.split(","))
                    .map(String::trim)
                    .filter(s -> !s.isEmpty())
                    .map(Long::valueOf)
                    .map(staffId -> {
                        StaffOnJob record = staffOnJobMapper.selectById(staffId);
                        if (record == null) {
                            return null;
                        }
                        return userMapper.selectOne(
                                Wrappers.<SysUser>lambdaQuery()
                                        .eq(SysUser::getUserName, record.getStaffNo())
                        );
                    })
                    .filter(Objects::nonNull)
                    .map(SysUser::getUserId)
                    .collect(Collectors.toList());
            List<Long> userIds = resolveParticipantUserIds(application.getParticipants());
            if (!userIds.isEmpty()) {
                sysNoticeService.simpleNoticeByUser(
@@ -211,6 +190,44 @@
        return R.ok();
    }
    // å‚会人员台账id -> ç™»å½•用户id;同一登录账号在 sys_user å­˜åœ¨å¤šæ¡åŽ†å²è®°å½•æ—¶ï¼ŒæŒ‰æœªåˆ é™¤ä¼˜å…ˆå–ä¸€æ¡ï¼Œé¿å… selectOne æŠ› TooManyResultsException
    private List<Long> resolveParticipantUserIds(String participants) {
        if (StrUtil.isBlank(participants)) {
            return Collections.emptyList();
        }
        String cleanStr = participants.replaceAll("^\\[|\\]$", "").trim();
        if (cleanStr.isEmpty()) {
            return Collections.emptyList();
        }
        List<Long> staffIds = Arrays.stream(cleanStr.split(","))
                .map(String::trim)
                .filter(s -> !s.isEmpty())
                .map(Long::valueOf)
                .collect(Collectors.toList());
        if (staffIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<String> loginNames = staffOnJobMapper.selectBatchIds(staffIds).stream()
                .map(StaffOnJob::getStaffNo)
                .filter(StrUtil::isNotBlank)
                .distinct()
                .collect(Collectors.toList());
        if (loginNames.isEmpty()) {
            return Collections.emptyList();
        }
        return userMapper.selectList(Wrappers.<SysUser>lambdaQuery()
                        .in(SysUser::getUserName, loginNames)
                        .orderByAsc(SysUser::getDelFlag)
                        .orderByAsc(SysUser::getUserId))
                .stream()
                .collect(Collectors.toMap(SysUser::getUserName, SysUser::getUserId,
                        (first, ignored) -> first, LinkedHashMap::new))
                .values()
                .stream()
                .distinct()
                .collect(Collectors.toList());
    }
    @Override
    public IPage<MeetApplication> getMeetingApplicationList(SearchMeetingApplicationVo vo) {
        LambdaQueryWrapper<MeetApplication> queryWrapper = new LambdaQueryWrapper<>();
src/main/java/com/ruoyi/common/enums/PurchaseApplicationStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package com.ruoyi.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * é‡‡è´­ç”³è¯·çŠ¶æ€æžšä¸¾
 */
@Getter
@AllArgsConstructor
public enum PurchaseApplicationStatusEnum implements BaseEnum<Integer> {
    DRAFT(0, "草稿"),
    ORDER_GENERATED(1, "已生成订单");
    private final Integer value;
    private final String label;
    @Override
    public Integer getCode() {
        return value;
    }
    @Override
    public String getValue() {
        return label;
    }
    public static String getLabelByValue(Integer value) {
        if (value == null) {
            return "未知状态";
        }
        for (PurchaseApplicationStatusEnum status : values()) {
            if (status.getCode().equals(value)) {
                return status.getValue();
            }
        }
        return "未知状态";
    }
}
src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceServiceImpl.java
@@ -18,7 +18,9 @@
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.measuringinstrumentledger.mapper.SparePartsMapper;
import com.ruoyi.measuringinstrumentledger.pojo.SpareParts;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRecord;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRequisitionRecord;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRecordService;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRequisitionRecordService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
@@ -38,6 +40,7 @@
    private final DeviceMaintenanceMapper deviceMaintenanceMapper;
    private final SparePartsMapper sparePartsMapper;
    private final SparePartsRequisitionRecordService sparePartsRequisitionRecordService;
    private final SparePartsRecordService sparePartsRecordService;
    private final FileUtil fileUtil;
    @Override
@@ -84,6 +87,15 @@
                        record.setSparePartsId(sparePartUse.getId());
                        record.setQuantity(sparePartUse.getQuantity());
                        sparePartsRequisitionRecordService.save(record);
                        // è¿½åŠ å¤‡ä»¶å‡ºåº“æµæ°´ï¼ˆ3 ä¿å…»é¢†ç”¨ï¼‰
                        SparePartsRecord outRecord = new SparePartsRecord();
                        outRecord.setSparePartsId(sparePartUse.getId());
                        outRecord.setDirection(2);
                        outRecord.setQuantity(new BigDecimal(sparePartUse.getQuantity()));
                        outRecord.setSourceType(3);
                        outRecord.setSourceId(deviceMaintenance.getId());
                        sparePartsRecordService.save(outRecord);
                    } else {
                        return AjaxResult.error("备件 " + spareParts.getName() + " æ•°é‡ä¸è¶³");
                    }
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java
@@ -21,7 +21,9 @@
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.measuringinstrumentledger.mapper.SparePartsMapper;
import com.ruoyi.measuringinstrumentledger.pojo.SpareParts;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRecord;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRequisitionRecord;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRecordService;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRequisitionRecordService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
@@ -45,6 +47,7 @@
    private final IDeviceLedgerService deviceLedgerService;
    private final SparePartsMapper sparePartsMapper;
    private final SparePartsRequisitionRecordService sparePartsRequisitionRecordService;
    private final SparePartsRecordService sparePartsRecordService;
    private final FileUtil fileUtil;
    private static final int STATUS_PENDING_REPAIR = 0;
@@ -114,6 +117,15 @@
                        record.setSparePartsId(sparePartUse.getId());
                        record.setQuantity(sparePartUse.getQuantity());
                        sparePartsRequisitionRecordService.save(record);
                        // è¿½åŠ å¤‡ä»¶å‡ºåº“æµæ°´ï¼ˆ2 ç»´ä¿®é¢†ç”¨ï¼‰
                        SparePartsRecord outRecord = new SparePartsRecord();
                        outRecord.setSparePartsId(sparePartUse.getId());
                        outRecord.setDirection(2);
                        outRecord.setQuantity(new BigDecimal(sparePartUse.getQuantity()));
                        outRecord.setSourceType(2);
                        outRecord.setSourceId(deviceRepairDto.getId());
                        sparePartsRecordService.save(outRecord);
                    } else {
                        return AjaxResult.error("备件 " + spareParts.getName() + " æ•°é‡ä¸è¶³");
                    }
src/main/java/com/ruoyi/measuringinstrumentledger/controller/SparePartsRecordController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package com.ruoyi.measuringinstrumentledger.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRecord;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRecordService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * å¤‡ä»¶å‡ºå…¥åº“流水
 *
 * @author :yys
 */
@RestController
@Tag(name = "备件出入库流水")
@RequestMapping("/sparePartsRecord")
@AllArgsConstructor
public class SparePartsRecordController extends BaseController {
    private SparePartsRecordService sparePartsRecordService;
    @GetMapping("/listPage")
    @Log(title = "备件出入库流水-分页查询", businessType = BusinessType.OTHER)
    public AjaxResult listPage(Page page, SparePartsRecord sparePartsRecord) {
        IPage<SparePartsRecord> listPage = sparePartsRecordService.listPage(page, sparePartsRecord);
        return AjaxResult.success(listPage);
    }
    @DeleteMapping("/delete")
    @Log(title = "备件出入库流水-删除", businessType = BusinessType.DELETE)
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return AjaxResult.error("请选择至少一条数据");
        }
        boolean delete = sparePartsRecordService.removeBatchByIds(ids);
        return delete ? AjaxResult.success() : AjaxResult.error();
    }
}
src/main/java/com/ruoyi/measuringinstrumentledger/mapper/SparePartsRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.ruoyi.measuringinstrumentledger.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRecord;
import org.apache.ibatis.annotations.Param;
/**
 * @author :yys
 */
public interface SparePartsRecordMapper extends BaseMapper<SparePartsRecord> {
    IPage<SparePartsRecord> listPage(Page page, @Param("req") SparePartsRecord req);
}
src/main/java/com/ruoyi/measuringinstrumentledger/pojo/SpareParts.java
@@ -46,6 +46,14 @@
     */
    private Long parentId;
    /**
     * å…³è”产品大类id
     */
    private Long productId;
    /**
     * å…³è”产品规格id(采购台账明细按此匹配备件)
     */
    private Long productModelId;
    /**
     * å¤‡ä»¶çŠ¶æ€
     */
    private String status;
src/main/java/com/ruoyi/measuringinstrumentledger/pojo/SparePartsRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
package com.ruoyi.measuringinstrumentledger.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å¤‡ä»¶å‡ºå…¥åº“流水
 *
 * @author :yys
 */
@Getter
@Setter
@TableName("spare_parts_record")
@Schema(name = "SparePartsRecord对象", description = "备件出入库流水")
public class SparePartsRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "备件id")
    private Long sparePartsId;
    @Schema(description = "方向 1入库 2出库")
    private Integer direction;
    @Schema(description = "数量")
    private BigDecimal quantity;
    @Schema(description = "来源类型 1采购入库 2维修领用 3保养领用")
    private Integer sourceType;
    @Schema(description = "来源单据id")
    private Long sourceId;
    @Schema(description = "来源单号")
    private String sourceNo;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "备件名称")
    @TableField(exist = false)
    private String sparePartsName;
    @Schema(description = "租户id")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @Schema(description = "录入时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/measuringinstrumentledger/service/SparePartsRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.measuringinstrumentledger.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRecord;
/**
 * @author :yys
 */
public interface SparePartsRecordService extends IService<SparePartsRecord> {
    IPage<SparePartsRecord> listPage(Page page, SparePartsRecord req);
}
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/SparePartsRecordServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package com.ruoyi.measuringinstrumentledger.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.measuringinstrumentledger.mapper.SparePartsRecordMapper;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRecord;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRecordService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
 * @author :yys
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class SparePartsRecordServiceImpl extends ServiceImpl<SparePartsRecordMapper, SparePartsRecord> implements SparePartsRecordService {
    private final SparePartsRecordMapper sparePartsRecordMapper;
    @Override
    public IPage<SparePartsRecord> listPage(Page page, SparePartsRecord req) {
        return sparePartsRecordMapper.listPage(page, req);
    }
}
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementDto.java
@@ -16,6 +16,11 @@
    private String purchaseContractNumber;
    /**
     * é‡‡è´­å°è´¦id,新增入库选订单后精确反显明细用(优于按合同号模糊匹配)
     */
    private Integer purchaseLedgerId;
    private Integer id;
    private Integer recordId;
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutAdd.java
@@ -26,4 +26,9 @@
    private Long productModelId;// åŸºç¡€äº§å“ID
    /**
     * å‡ºåº“批次,销售出库必填(扣库存按批次扣)
     */
    private String batchNo;
}
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutPageDto.java
@@ -18,6 +18,17 @@
    @Excel(name = "出库编号")
    private String code;
    /**
     * å‡ºåº“批次
     */
    private String batchNo;
    /**
     * è‡ªåŠ¨ç”Ÿæˆçš„å‘è´§å•å·ï¼ˆé”€å”®å‡ºåº“æ‰ç®—å‘è´§ï¼‰
     */
    @Excel(name = "发货单号")
    private String shippingNo;
    private Integer id;
    private BigDecimal warnNum;
src/main/java/com/ruoyi/procurementrecord/bean/vo/BatchNoOptionVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.ruoyi.procurementrecord.bean.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * å‡ºåº“可选批次:批次号 + å½“前可用量
 *
 * @author :yys
 */
@Data
@Schema(name = "BatchNoOptionVo", description = "出库可选批次")
public class BatchNoOptionVo {
    @Schema(description = "规格型号id")
    private Long productModelId;
    @Schema(description = "批号")
    private String batchNo;
    @Schema(description = "可用量(库存量 - é”å®šæ•°é‡ï¼‰")
    private BigDecimal availableQuantity;
}
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementRecordOutController.java
@@ -45,6 +45,15 @@
        return AjaxResult.success(result);
    }
    /**
     * é”€å”®å‡ºåº“选批次:按规格型号查可选批次与可用量
     */
    @GetMapping("/batchNoList")
    @Log(title = "采购出库-出库管理-批次列表", businessType = BusinessType.OTHER)
    public AjaxResult batchNoList(@RequestParam("productModelId") Long productModelId) {
        return AjaxResult.success(procurementRecordOutService.listBatchNoByProductModelId(productModelId));
    }
    @GetMapping("/listPageByProduct")
    @Log(title = "生产出库-出库台账-出库查询", businessType = BusinessType.OTHER)
    public AjaxResult listPageByProduct(Page page, ProcurementRecordOutPageDto procurementDto) {
src/main/java/com/ruoyi/procurementrecord/mapper/ProcurementRecordOutMapper.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.bean.vo.BatchNoOptionVo;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import org.apache.ibatis.annotations.Param;
@@ -33,4 +34,9 @@
    ProcurementRecordOut selectCode(@Param("format") String format);
    IPage<ProcurementRecordOutPageDto> listPageBySemiProduct(Page page,@Param("req") ProcurementRecordOutPageDto procurementDto);
    /**
     * æŸ¥è¯¢æŸè§„格型号下可选批次及可用量(销售出库选批次用)
     */
    List<BatchNoOptionVo> listBatchNoByProductModelId(@Param("productModelId") Long productModelId);
}
src/main/java/com/ruoyi/procurementrecord/pojo/ProcurementRecordOut.java
@@ -92,6 +92,16 @@
     */
    private Long productModelId;
    /**
     * å‡ºåº“批次(扣库存用)
     */
    private String batchNo;
    /**
     * é”€å”®å‡ºåº“时自动生成的发货台账id
     */
    private Long shippingInfoId;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
src/main/java/com/ruoyi/procurementrecord/service/ProcurementRecordOutService.java
@@ -6,9 +6,11 @@
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutAdd;
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementUpdateDto;
import com.ruoyi.procurementrecord.bean.vo.BatchNoOptionVo;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * @author :yys
@@ -29,4 +31,9 @@
    IPage<ProcurementRecordOutPageDto> listPageByCustom(Page page, ProcurementRecordOutPageDto procurementDto);
    IPage<ProcurementRecordOutPageDto> listPageBySemiProduct(Page page, ProcurementRecordOutPageDto procurementDto);
    /**
     * é”€å”®å‡ºåº“选批次:返回该规格型号下的可选批次与可用量
     */
    List<BatchNoOptionVo> listBatchNoByProductModelId(Long productModelId);
}
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordOutServiceImpl.java
@@ -8,15 +8,22 @@
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutAdd;
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementUpdateDto;
import com.ruoyi.procurementrecord.bean.vo.BatchNoOptionVo;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import com.ruoyi.procurementrecord.service.ProcurementRecordOutService;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.ShippingProductDetail;
import com.ruoyi.sales.service.ShippingInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import jakarta.servlet.http.HttpServletResponse;
@@ -25,6 +32,7 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@@ -37,11 +45,26 @@
@Slf4j
public class ProcurementRecordOutServiceImpl extends ServiceImpl<ProcurementRecordOutMapper, ProcurementRecordOut> implements ProcurementRecordOutService {
    /**
     * å‡ºåº“类型:销售出库。油品销售出库即发货
     */
    private static final Integer TYPE_SALE_OUT = 2;
    /**
     * é”€å”®äº§å“æ˜Žç»†ç±»åž‹ï¼š1销售(sales_ledger_product.type,与出库 type è¯­ä¹‰ä¸åŒï¼‰
     */
    private static final Integer LEDGER_PRODUCT_TYPE_SALE = 1;
    public final ProcurementRecordOutMapper procurementRecordOutMapper;
    public final SysUserMapper sysUserMapper;
    public final ShippingInfoService shippingInfoService;
    public final SalesLedgerProductMapper salesLedgerProductMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int stockout(ProcurementRecordOutAdd procurementRecordOutAdd) {
        SysUser sysUser = sysUserMapper.selectUserById(Long.valueOf(procurementRecordOutAdd.getUserId().toString()));
        if(sysUser == null){
@@ -79,9 +102,40 @@
                .createBy(sysUser.getNickName())
                .updateUser(Long.valueOf(procurementRecordOutAdd.getUserId()))
                .updateTime(LocalDateTime.now())
                .productModelId(procurementRecordOutAdd.getProductModelId());
        this.save(procurementRecordOut.build());
                .productModelId(procurementRecordOutAdd.getProductModelId())
                .batchNo(procurementRecordOutAdd.getBatchNo());
        ProcurementRecordOut record = procurementRecordOut.build();
        this.save(record);
        // æ²¹å“é”€å”®å‡ºåº“即发货:自动生成一条已发货的发货台账并回写关联
        if (TYPE_SALE_OUT.equals(record.getType())) {
            record.setShippingInfoId(createShippingForOutbound(record));
            this.updateById(record);
        }
        return 0;
    }
    /**
     * æŒ‰å‡ºåº“记录生成发货台账:批次必填,库存由发货侧扣减
     */
    private Long createShippingForOutbound(ProcurementRecordOut record) {
        if (StringUtils.isBlank(record.getBatchNo())) {
            throw new RuntimeException("请选择出库批次");
        }
        SalesLedgerProduct salesLedgerProduct = salesLedgerProductMapper.selectById(record.getSalesLedgerProductId());
        if (salesLedgerProduct == null || !LEDGER_PRODUCT_TYPE_SALE.equals(salesLedgerProduct.getType())) {
            throw new RuntimeException("销售出库未关联到有效的销售产品明细");
        }
        ShippingProductDetail detail = new ShippingProductDetail();
        detail.setProductModelId(record.getProductModelId());
        detail.setQuantity(record.getInboundNum());
        detail.setBatchNo(record.getBatchNo());
        ShippingInfoDto shippingInfoDto = new ShippingInfoDto();
        shippingInfoDto.setSalesLedgerId(salesLedgerProduct.getSalesLedgerId());
        shippingInfoDto.setSalesLedgerProductId(record.getSalesLedgerProductId());
        shippingInfoDto.setBatchNoDetailList(Collections.singletonList(detail));
        return shippingInfoService.addShippedFromOutbound(shippingInfoDto);
    }
    @Override
@@ -94,10 +148,17 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int deletePro(ProcurementUpdateDto procurementDto) {
        List<ProcurementRecordOut> procurementRecordOutByIds = getProcurementRecordOutByIds(procurementDto.getIds());
        if(CollectionUtils.isEmpty(procurementRecordOutByIds)){
            throw new RuntimeException("未查询到选中的人");
        }
        // é”€å”®å‡ºåº“已生成发货单,删除出库单前先冲销发货单并归还已扣库存
        for (ProcurementRecordOut record : procurementRecordOutByIds) {
            if (record.getShippingInfoId() != null) {
                shippingInfoService.cancelByOutbound(record.getShippingInfoId());
            }
        }
        procurementRecordOutMapper.deleteBatchIds(procurementRecordOutByIds.stream().map(ProcurementRecordOut::getId).collect(Collectors.toList()));
        return 0;
@@ -124,4 +185,12 @@
    public IPage<ProcurementRecordOutPageDto> listPageBySemiProduct(Page page, ProcurementRecordOutPageDto procurementDto) {
        return procurementRecordOutMapper.listPageBySemiProduct(page, procurementDto);
    }
    @Override
    public List<BatchNoOptionVo> listBatchNoByProductModelId(Long productModelId) {
        if (productModelId == null) {
            return Collections.emptyList();
        }
        return procurementRecordOutMapper.listBatchNoByProductModelId(productModelId);
    }
}
src/main/java/com/ruoyi/purchase/controller/PurchaseApplicationController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
package com.ruoyi.purchase.controller;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.purchase.dto.PurchaseApplicationDto;
import com.ruoyi.purchase.pojo.PurchaseApplication;
import com.ruoyi.purchase.service.PurchaseApplicationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * é‡‡è´­ç”³è¯·å• Controller
 */
@RestController
@RequestMapping("/purchase/application")
@AllArgsConstructor
@Tag(name = "采购申请")
public class PurchaseApplicationController extends BaseController {
    private final PurchaseApplicationService purchaseApplicationService;
    /**
     * æŸ¥è¯¢é‡‡è´­ç”³è¯·åˆ—表
     */
    @GetMapping("/list")
    public TableDataInfo list(PurchaseApplicationDto purchaseApplicationDto) {
        startPage();
        List<PurchaseApplication> list = purchaseApplicationService.selectApplicationList(purchaseApplicationDto);
        return getDataTable(list);
    }
    /**
     * æŸ¥è¯¢é‡‡è´­ç”³è¯·è¯¦æƒ…(含产品明细)
     */
    @GetMapping("/getById")
    public AjaxResult getById(@RequestParam Long id) {
        return AjaxResult.success(purchaseApplicationService.getDetail(id));
    }
    /**
     * æ–°å¢žé‡‡è´­ç”³è¯·ï¼ˆè‰ç¨¿ï¼‰
     */
    @Log(title = "采购申请", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody PurchaseApplicationDto purchaseApplicationDto) {
        return AjaxResult.success(purchaseApplicationService.saveApplication(purchaseApplicationDto));
    }
    /**
     * ä¿®æ”¹é‡‡è´­ç”³è¯·ï¼Œä»…草稿状态允许修改
     */
    @Log(title = "采购申请", businessType = BusinessType.UPDATE)
    @PostMapping("/edit")
    public AjaxResult edit(@RequestBody PurchaseApplicationDto purchaseApplicationDto) {
        return toAjax(purchaseApplicationService.updateApplication(purchaseApplicationDto));
    }
    /**
     * åˆ é™¤é‡‡è´­ç”³è¯·ï¼Œä»…草稿状态允许删除
     */
    @Log(title = "采购申请", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    public AjaxResult delete(@RequestBody Long[] ids) {
        return toAjax(purchaseApplicationService.deleteApplications(
                ids == null ? List.of() : List.of(ids)));
    }
    /**
     * é‡‡è´­ç”³è¯·è½¬è®¢å•:生成采购台账并提交 OA é‡‡è´­å®¡æ‰¹
     */
    @Operation(summary = "采购申请转订单")
    @Log(title = "采购申请转订单", businessType = BusinessType.OTHER)
    @PostMapping("/convertToOrder")
    public AjaxResult convertToOrder(@RequestParam Long id) {
        return AjaxResult.success(purchaseApplicationService.convertToOrder(id));
    }
}
src/main/java/com/ruoyi/purchase/dto/PurchaseApplicationDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
package com.ruoyi.purchase.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.purchase.pojo.PurchaseApplicationProduct;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
/**
 * é‡‡è´­ç”³è¯·å• DTO
 */
@Data
public class PurchaseApplicationDto {
    @Schema(description = "申请单主键")
    private Long id;
    @Schema(description = "申请单号")
    private String applicationNo;
    @Schema(description = "供应商id")
    private Long supplierId;
    @Schema(description = "供应商名称")
    private String supplierName;
    @Schema(description = "项目名称")
    private String projectName;
    @Schema(description = "销售合同号")
    private String salesContractNo;
    @Schema(description = "关联销售台账主表主键")
    private Long salesLedgerId;
    @Schema(description = "申请人id")
    private Long applicantId;
    @Schema(description = "申请人姓名")
    private String applicantName;
    @Schema(description = "申请日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate applyDate;
    @Schema(description = "期望到货日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate expectedArrivalDate;
    @Schema(description = "申请金额(含税总价)")
    private BigDecimal contractAmount;
    @Schema(description = "付款方式")
    private String paymentMethod;
    @Schema(description = "备注")
    private String remarks;
    @Schema(description = "状态 0草稿 1已生成订单")
    private Integer status;
    @Schema(description = "转订单后生成的采购台账id")
    private Long purchaseLedgerId;
    @Schema(description = "申请日期查询开始(yyyy-MM-dd)")
    private String startDate;
    @Schema(description = "申请日期查询结束(yyyy-MM-dd)")
    private String endDate;
    @Schema(description = "采购申请产品明细")
    private List<PurchaseApplicationProduct> productData;
}
src/main/java/com/ruoyi/purchase/mapper/PurchaseApplicationMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.ruoyi.purchase.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.purchase.pojo.PurchaseApplication;
import org.apache.ibatis.annotations.Mapper;
/**
 * é‡‡è´­ç”³è¯·å• Mapper æŽ¥å£
 */
@Mapper
public interface PurchaseApplicationMapper extends BaseMapper<PurchaseApplication> {
}
src/main/java/com/ruoyi/purchase/mapper/PurchaseApplicationProductMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.ruoyi.purchase.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.purchase.pojo.PurchaseApplicationProduct;
import org.apache.ibatis.annotations.Mapper;
/**
 * é‡‡è´­ç”³è¯·äº§å“æ˜Žç»† Mapper æŽ¥å£
 */
@Mapper
public interface PurchaseApplicationProductMapper extends BaseMapper<PurchaseApplicationProduct> {
}
src/main/java/com/ruoyi/purchase/pojo/PurchaseApplication.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
package com.ruoyi.purchase.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
 * é‡‡è´­ç”³è¯·å•对象 purchase_application
 */
@Getter
@Setter
@TableName("purchase_application")
@Schema(name = "PurchaseApplication对象", description = "采购申请单")
public class PurchaseApplication implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "申请单号")
    private String applicationNo;
    @Schema(description = "供应商id")
    private Long supplierId;
    @Schema(description = "供应商名称")
    private String supplierName;
    @Schema(description = "项目名称")
    private String projectName;
    @Schema(description = "销售合同号")
    private String salesContractNo;
    @Schema(description = "关联销售台账主表主键")
    private Long salesLedgerId;
    @Schema(description = "申请人id")
    private Long applicantId;
    @Schema(description = "申请人姓名")
    private String applicantName;
    @Schema(description = "申请日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate applyDate;
    @Schema(description = "期望到货日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate expectedArrivalDate;
    @Schema(description = "申请金额(含税总价)")
    private BigDecimal contractAmount;
    @Schema(description = "付款方式")
    private String paymentMethod;
    @Schema(description = "备注")
    private String remarks;
    @Schema(description = "状态 0草稿 1已生成订单")
    private Integer status;
    @Schema(description = "转订单后生成的采购台账id")
    private Long purchaseLedgerId;
    @Schema(description = "转订单时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime convertTime;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @TableField(exist = false)
    @Schema(description = "采购申请产品明细")
    private List<PurchaseApplicationProduct> productData;
}
src/main/java/com/ruoyi/purchase/pojo/PurchaseApplicationProduct.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
package com.ruoyi.purchase.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * é‡‡è´­ç”³è¯·äº§å“æ˜Žç»†å¯¹è±¡ purchase_application_product
 */
@Getter
@Setter
@TableName("purchase_application_product")
@Schema(name = "PurchaseApplicationProduct对象", description = "采购申请产品明细")
public class PurchaseApplicationProduct implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "采购申请单id")
    private Long purchaseApplicationId;
    @Schema(description = "产品id")
    private Long productId;
    @Schema(description = "产品规格id")
    private Long productModelId;
    @Schema(description = "产品大类")
    private String productCategory;
    @Schema(description = "规格型号")
    private String specificationModel;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "数量")
    private BigDecimal quantity;
    @Schema(description = "税率")
    private BigDecimal taxRate;
    @Schema(description = "含税单价")
    private BigDecimal taxInclusiveUnitPrice;
    @Schema(description = "含税总价")
    private BigDecimal taxInclusiveTotalPrice;
    @Schema(description = "不含税总价")
    private BigDecimal taxExclusiveTotalPrice;
    @Schema(description = "发票类型")
    private String invoiceType;
    @Schema(description = "是否推送质检")
    private Boolean isChecked;
    @Schema(description = "备注")
    private String remark;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
}
src/main/java/com/ruoyi/purchase/service/PurchaseApplicationService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package com.ruoyi.purchase.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.purchase.dto.PurchaseApplicationDto;
import com.ruoyi.purchase.pojo.PurchaseApplication;
import java.util.List;
/**
 * é‡‡è´­ç”³è¯·å• Service æŽ¥å£
 */
public interface PurchaseApplicationService extends IService<PurchaseApplication> {
    /**
     * æŸ¥è¯¢é‡‡è´­ç”³è¯·åˆ—表
     */
    List<PurchaseApplication> selectApplicationList(PurchaseApplicationDto purchaseApplicationDto);
    /**
     * æŸ¥è¯¢é‡‡è´­ç”³è¯·è¯¦æƒ…(含产品明细)
     */
    PurchaseApplicationDto getDetail(Long id);
    /**
     * æ–°å¢žé‡‡è´­ç”³è¯·ï¼ˆè‰ç¨¿ï¼‰
     */
    Long saveApplication(PurchaseApplicationDto purchaseApplicationDto);
    /**
     * ç¼–辑采购申请,仅草稿状态允许编辑
     */
    boolean updateApplication(PurchaseApplicationDto purchaseApplicationDto);
    /**
     * åˆ é™¤é‡‡è´­ç”³è¯·ï¼Œä»…草稿状态允许删除
     */
    boolean deleteApplications(List<Long> ids);
    /**
     * é‡‡è´­ç”³è¯·è½¬è®¢å•:生成采购台账并提交 OA é‡‡è´­å®¡æ‰¹ï¼Œè¿”回采购台账id
     */
    Long convertToOrder(Long id);
}
src/main/java/com/ruoyi/purchase/service/impl/PurchaseApplicationServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,346 @@
package com.ruoyi.purchase.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.common.enums.PurchaseApplicationStatusEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.purchase.dto.PurchaseApplicationDto;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.mapper.PurchaseApplicationMapper;
import com.ruoyi.purchase.mapper.PurchaseApplicationProductMapper;
import com.ruoyi.purchase.pojo.PurchaseApplication;
import com.ruoyi.purchase.pojo.PurchaseApplicationProduct;
import com.ruoyi.purchase.service.IPurchaseLedgerService;
import com.ruoyi.purchase.service.PurchaseApplicationService;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * é‡‡è´­ç”³è¯·å• Service ä¸šåŠ¡å±‚å¤„ç†
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class PurchaseApplicationServiceImpl extends ServiceImpl<PurchaseApplicationMapper, PurchaseApplication>
        implements PurchaseApplicationService {
    private static final String APPLICATION_NO_PREFIX = "CGSQ";
    private final PurchaseApplicationMapper purchaseApplicationMapper;
    private final PurchaseApplicationProductMapper purchaseApplicationProductMapper;
    private final IPurchaseLedgerService purchaseLedgerService;
    private final SupplierManageMapper supplierManageMapper;
    private final SysUserMapper sysUserMapper;
    private final ProductMapper productMapper;
    private final ProductModelMapper productModelMapper;
    @Override
    public List<PurchaseApplication> selectApplicationList(PurchaseApplicationDto purchaseApplicationDto) {
        LambdaQueryWrapper<PurchaseApplication> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(StringUtils.isNotBlank(purchaseApplicationDto.getApplicationNo()),
                        PurchaseApplication::getApplicationNo, purchaseApplicationDto.getApplicationNo())
                .like(StringUtils.isNotBlank(purchaseApplicationDto.getProjectName()),
                        PurchaseApplication::getProjectName, purchaseApplicationDto.getProjectName())
                .eq(purchaseApplicationDto.getSupplierId() != null,
                        PurchaseApplication::getSupplierId, purchaseApplicationDto.getSupplierId())
                .eq(purchaseApplicationDto.getStatus() != null,
                        PurchaseApplication::getStatus, purchaseApplicationDto.getStatus())
                .ge(StringUtils.isNotBlank(purchaseApplicationDto.getStartDate()),
                        PurchaseApplication::getApplyDate, purchaseApplicationDto.getStartDate())
                .le(StringUtils.isNotBlank(purchaseApplicationDto.getEndDate()),
                        PurchaseApplication::getApplyDate, purchaseApplicationDto.getEndDate())
                .orderByDesc(PurchaseApplication::getId);
        return this.list(queryWrapper);
    }
    @Override
    public PurchaseApplicationDto getDetail(Long id) {
        PurchaseApplication application = this.getById(id);
        if (application == null) {
            throw new ServiceException("采购申请不存在");
        }
        PurchaseApplicationDto resultDto = new PurchaseApplicationDto();
        BeanUtils.copyProperties(application, resultDto);
        resultDto.setProductData(listProducts(id));
        return resultDto;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long saveApplication(PurchaseApplicationDto purchaseApplicationDto) {
        PurchaseApplication application = new PurchaseApplication();
        BeanUtils.copyProperties(purchaseApplicationDto, application);
        application.setId(null);
        application.setStatus(PurchaseApplicationStatusEnum.DRAFT.getCode());
        application.setPurchaseLedgerId(null);
        application.setConvertTime(null);
        application.setApplicationNo(OrderUtils.countTodayByCreateTime(
                purchaseApplicationMapper, APPLICATION_NO_PREFIX, "application_no", LocalDateTime.now()));
        fillSupplierName(application);
        fillApplicant(application, purchaseApplicationDto);
        this.save(application);
        saveProducts(application.getId(), purchaseApplicationDto.getProductData());
        return application.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateApplication(PurchaseApplicationDto purchaseApplicationDto) {
        if (purchaseApplicationDto.getId() == null) {
            throw new ServiceException("采购申请id不能为空");
        }
        PurchaseApplication existing = this.getById(purchaseApplicationDto.getId());
        if (existing == null) {
            throw new ServiceException("采购申请不存在");
        }
        if (!PurchaseApplicationStatusEnum.DRAFT.getCode().equals(existing.getStatus())) {
            throw new ServiceException("已生成订单的采购申请不允许修改");
        }
        PurchaseApplication application = new PurchaseApplication();
        BeanUtils.copyProperties(purchaseApplicationDto, application);
        // å•号、状态、订单关联由系统维护,不允许前端改动;置空后 MyBatis-Plus ä¸ä¼šæ›´æ–°è¿™äº›åˆ—
        application.setApplicationNo(null);
        application.setStatus(null);
        application.setPurchaseLedgerId(null);
        application.setConvertTime(null);
        application.setTenantId(null);
        application.setCreateUser(null);
        application.setCreateTime(null);
        fillSupplierName(application);
        boolean updated = this.updateById(application);
        if (updated) {
            purchaseApplicationProductMapper.delete(Wrappers.<PurchaseApplicationProduct>lambdaQuery()
                    .eq(PurchaseApplicationProduct::getPurchaseApplicationId, purchaseApplicationDto.getId()));
            saveProducts(purchaseApplicationDto.getId(), purchaseApplicationDto.getProductData());
        }
        return updated;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteApplications(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new ServiceException("请选择要删除的采购申请");
        }
        List<String> orderedNos = this.listByIds(ids).stream()
                .filter(application -> !PurchaseApplicationStatusEnum.DRAFT.getCode().equals(application.getStatus()))
                .map(PurchaseApplication::getApplicationNo)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (!orderedNos.isEmpty()) {
            throw new ServiceException("已生成订单的采购申请不允许删除:" + String.join("、", orderedNos));
        }
        purchaseApplicationProductMapper.delete(Wrappers.<PurchaseApplicationProduct>lambdaQuery()
                .in(PurchaseApplicationProduct::getPurchaseApplicationId, ids));
        return this.removeByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long convertToOrder(Long id) {
        if (id == null) {
            throw new ServiceException("采购申请id不能为空");
        }
        PurchaseApplication application = this.getById(id);
        if (application == null) {
            throw new ServiceException("采购申请不存在");
        }
        if (application.getSupplierId() == null) {
            throw new ServiceException("请先选择供应商再转订单");
        }
        SupplierManage supplierManage = supplierManageMapper.selectById(application.getSupplierId());
        if (supplierManage == null) {
            throw new ServiceException("供应商不存在,请重新选择");
        }
        List<PurchaseApplicationProduct> products = listProducts(id);
        if (products.isEmpty()) {
            throw new ServiceException("采购申请没有产品明细,无法转订单");
        }
        // æ¡ä»¶æ›´æ–°æŠ¢å è‰ç¨¿çŠ¶æ€ï¼Œé¿å…å¹¶å‘ä¸‹é‡å¤ç”Ÿæˆè®¢å•
        boolean converted = this.update(Wrappers.<PurchaseApplication>lambdaUpdate()
                .eq(PurchaseApplication::getId, id)
                .eq(PurchaseApplication::getStatus, PurchaseApplicationStatusEnum.DRAFT.getCode())
                .set(PurchaseApplication::getStatus, PurchaseApplicationStatusEnum.ORDER_GENERATED.getCode()));
        if (!converted) {
            throw new ServiceException("该采购申请已生成订单,请勿重复转换");
        }
        PurchaseLedgerDto purchaseLedgerDto = buildPurchaseLedgerDto(application, supplierManage, products);
        try {
            // å¤ç”¨é‡‡è´­å°è´¦æ–°å¢žé€»è¾‘:生成采购合同号、保存台账与产品明细、提交 OA é‡‡è´­å®¡æ‰¹
            purchaseLedgerService.addOrEditPurchase(purchaseLedgerDto);
        } catch (Exception e) {
            log.error("采购申请转订单失败, applicationId={}, applicationNo={}",
                    id, application.getApplicationNo(), e);
            throw new ServiceException("转订单失败:" + e.getMessage());
        }
        Long purchaseLedgerId = purchaseLedgerDto.getId();
        if (purchaseLedgerId == null) {
            throw new ServiceException("转订单失败:未生成采购订单");
        }
        this.update(Wrappers.<PurchaseApplication>lambdaUpdate()
                .eq(PurchaseApplication::getId, id)
                .set(PurchaseApplication::getPurchaseLedgerId, purchaseLedgerId)
                .set(PurchaseApplication::getConvertTime, LocalDateTime.now()));
        return purchaseLedgerId;
    }
    private List<PurchaseApplicationProduct> listProducts(Long applicationId) {
        return purchaseApplicationProductMapper.selectList(Wrappers.<PurchaseApplicationProduct>lambdaQuery()
                .eq(PurchaseApplicationProduct::getPurchaseApplicationId, applicationId)
                .orderByAsc(PurchaseApplicationProduct::getId));
    }
    private void saveProducts(Long applicationId, List<PurchaseApplicationProduct> products) {
        if (products == null || products.isEmpty()) {
            return;
        }
        fillProductNames(products);
        for (PurchaseApplicationProduct product : products) {
            product.setId(null);
            product.setPurchaseApplicationId(applicationId);
            product.setTenantId(null);
            product.setCreateUser(null);
            product.setCreateTime(null);
            purchaseApplicationProductMapper.insert(product);
        }
    }
    /**
     * è¡¥å…¨äº§å“å¤§ç±»ä¸Žè§„格型号,便于申请单直接展示产品名称
     */
    private void fillProductNames(List<PurchaseApplicationProduct> products) {
        Set<Long> productIds = products.stream()
                .map(PurchaseApplicationProduct::getProductId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Set<Long> modelIds = products.stream()
                .map(PurchaseApplicationProduct::getProductModelId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, String> productMap = new HashMap<>();
        if (!productIds.isEmpty()) {
            for (Product product : productMapper.selectBatchIds(productIds)) {
                productMap.put(product.getId(), product.getProductName());
            }
        }
        Map<Long, String> modelMap = new HashMap<>();
        if (!modelIds.isEmpty()) {
            for (ProductModel model : productModelMapper.selectBatchIds(modelIds)) {
                modelMap.put(model.getId(), model.getModel());
            }
        }
        for (PurchaseApplicationProduct product : products) {
            String productName = productMap.get(product.getProductId());
            if (productName != null) {
                product.setProductCategory(productName);
            }
            String model = modelMap.get(product.getProductModelId());
            if (model != null) {
                product.setSpecificationModel(model);
            }
        }
    }
    private void fillSupplierName(PurchaseApplication application) {
        if (application.getSupplierId() == null) {
            return;
        }
        SupplierManage supplierManage = supplierManageMapper.selectById(application.getSupplierId());
        if (supplierManage != null) {
            application.setSupplierName(supplierManage.getSupplierName());
        }
    }
    private void fillApplicant(PurchaseApplication application, PurchaseApplicationDto purchaseApplicationDto) {
        Long applicantId = purchaseApplicationDto.getApplicantId() != null
                ? purchaseApplicationDto.getApplicantId()
                : SecurityUtils.getUserId();
        application.setApplicantId(applicantId);
        if (StringUtils.isBlank(purchaseApplicationDto.getApplicantName())) {
            SysUser applicant = sysUserMapper.selectUserById(applicantId);
            application.setApplicantName(applicant != null ? applicant.getNickName() : SecurityUtils.getUsername());
        }
        if (application.getApplyDate() == null) {
            application.setApplyDate(LocalDate.now());
        }
    }
    /**
     * ç»„装采购台账入参:供应商、销售合同、产品明细等;采购合同号由采购台账侧生成
     */
    private PurchaseLedgerDto buildPurchaseLedgerDto(PurchaseApplication application,
                                                     SupplierManage supplierManage,
                                                     List<PurchaseApplicationProduct> products) {
        PurchaseLedgerDto purchaseLedgerDto = new PurchaseLedgerDto();
        purchaseLedgerDto.setSupplierId(application.getSupplierId());
        purchaseLedgerDto.setSupplierName(supplierManage.getSupplierName());
        purchaseLedgerDto.setRecorderId(application.getApplicantId());
        purchaseLedgerDto.setRecorderName(application.getApplicantName());
        purchaseLedgerDto.setSalesContractNo(application.getSalesContractNo());
        purchaseLedgerDto.setSalesLedgerId(application.getSalesLedgerId());
        purchaseLedgerDto.setProjectName(application.getProjectName());
        purchaseLedgerDto.setRemarks(application.getRemarks());
        purchaseLedgerDto.setPaymentMethod(application.getPaymentMethod());
        purchaseLedgerDto.setContractAmount(application.getContractAmount());
        LocalDate entryDate = application.getApplyDate() != null ? application.getApplyDate() : LocalDate.now();
        purchaseLedgerDto.setEntryDate(DateUtils.toDate(entryDate));
        purchaseLedgerDto.setProductData(products.stream()
                .map(this::toSalesLedgerProduct)
                .collect(Collectors.toList()));
        return purchaseLedgerDto;
    }
    private SalesLedgerProduct toSalesLedgerProduct(PurchaseApplicationProduct source) {
        SalesLedgerProduct target = new SalesLedgerProduct();
        target.setProductId(source.getProductId());
        target.setProductModelId(source.getProductModelId());
        target.setProductCategory(source.getProductCategory());
        target.setSpecificationModel(source.getSpecificationModel());
        target.setUnit(source.getUnit());
        target.setQuantity(source.getQuantity());
        target.setTaxRate(source.getTaxRate());
        target.setTaxInclusiveUnitPrice(source.getTaxInclusiveUnitPrice());
        target.setTaxInclusiveTotalPrice(source.getTaxInclusiveTotalPrice());
        target.setTaxExclusiveTotalPrice(source.getTaxExclusiveTotalPrice());
        target.setInvoiceType(source.getInvoiceType());
        target.setIsChecked(source.getIsChecked());
        return target;
    }
}
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -37,6 +37,10 @@
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.measuringinstrumentledger.mapper.SparePartsMapper;
import com.ruoyi.measuringinstrumentledger.pojo.SpareParts;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRecord;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRecordService;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
@@ -96,6 +100,11 @@
@RequiredArgsConstructor
public class PurchaseLedgerServiceImpl extends ServiceImpl<PurchaseLedgerMapper, PurchaseLedger> implements IPurchaseLedgerService {
    /**
     * æ˜Žç»†ç±»åž‹ï¼šé‡‡è´­ï¼ˆsales_ledger_product.type)。仅采购明细才回写备件库存
     */
    private static final Integer LEDGER_TYPE_PURCHASE = 2;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
@@ -121,6 +130,8 @@
    private final StockUtils stockUtils;
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final OilDepotMapper oilDepotMapper;
    private final SparePartsMapper sparePartsMapper;
    private final SparePartsRecordService sparePartsRecordService;
    @Override
    public List<PurchaseLedger> selectPurchaseLedgerList(PurchaseLedger purchaseLedger) {
@@ -190,6 +201,8 @@
            purchaseLedger.setPurchaseContractNumber(generatePurchaseContractNo(purchaseLedger.getEntryDate()));
        }
        this.saveOrUpdate(purchaseLedger);
        // å›žå†™ä¸»é”®ï¼Œä¾›è°ƒç”¨æ–¹ï¼ˆå¦‚采购申请转订单)获取新生成的采购台账id
        purchaseLedgerDto.setId(purchaseLedger.getId());
        // 4. å¤„理子表数据
        List<SalesLedgerProduct> productList = purchaseLedgerDto.getProductData();
@@ -706,6 +719,10 @@
                salesLedgerProduct.setRegisterDate(localDateTime);
                salesLedgerProductMapper.insert(salesLedgerProduct);
            }
            // ä»…新增明细回写备件,编辑走 update åˆ†æ”¯å¤©ç„¶ä¸é‡å¤è®¡æ•°
            if (LEDGER_TYPE_PURCHASE.equals(ledgerType)) {
                recordSparePartsInbound(insertList, purchaseLedger);
            }
        }
        // è®¡ç®—总含税金额
@@ -721,6 +738,47 @@
        }
    }
    /**
     * é‡‡è´­å°è´¦æ–°å¢žæ˜Žç»†æŒ‰è§„格匹配备件:累加备件库存并记一条入库流水。
     * æœªç»´æŠ¤è§„格的备件不参与,静默跳过。
     */
    private void recordSparePartsInbound(List<SalesLedgerProduct> insertList, PurchaseLedger purchaseLedger) {
        List<Long> modelIds = insertList.stream()
                .map(SalesLedgerProduct::getProductModelId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (modelIds.isEmpty()) {
            return;
        }
        List<SpareParts> sparePartsList = sparePartsMapper.selectList(new LambdaQueryWrapper<SpareParts>()
                .in(SpareParts::getProductModelId, modelIds));
        if (CollectionUtils.isEmpty(sparePartsList)) {
            return;
        }
        Map<Long, SpareParts> sparePartsByModelId = sparePartsList.stream()
                .collect(Collectors.toMap(SpareParts::getProductModelId, s -> s, (a, b) -> a));
        for (SalesLedgerProduct product : insertList) {
            SpareParts spareParts = sparePartsByModelId.get(product.getProductModelId());
            if (spareParts == null || product.getQuantity() == null
                    || product.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal quantity = product.getQuantity();
            spareParts.setQuantity((spareParts.getQuantity() == null ? BigDecimal.ZERO : spareParts.getQuantity()).add(quantity));
            sparePartsMapper.updateById(spareParts);
            SparePartsRecord record = new SparePartsRecord();
            record.setSparePartsId(spareParts.getId());
            record.setDirection(1);
            record.setQuantity(quantity);
            record.setSourceType(1);
            record.setSourceId(purchaseLedger.getId());
            record.setSourceNo(purchaseLedger.getPurchaseContractNumber());
            sparePartsRecordService.save(record);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int deletePurchaseLedgerByIds(Long[] ids) {
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java
@@ -27,6 +27,9 @@
    @Schema(description = "付款金额")
    private BigDecimal paymentAmount;
    @Schema(description = "回款金额")
    private BigDecimal receiptAmount;
    @Schema(description = "应付金额")
    private BigDecimal payableAmount;
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java
@@ -23,6 +23,10 @@
    //该供应商采购付款累计金额
    private BigDecimal paymentAmount;
    @Schema(description = "回款金额")
    //该供应商往来回款累计金额(payment_record æ‰‹å·¥æµæ°´ï¼‰
    private BigDecimal receiptAmount;
    @Schema(description = "应付金额")
    //该供应商采购应付累计金额=初始金额-付款-退款
    private BigDecimal payableAmount;
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java
@@ -33,4 +33,16 @@
    ShippingApproveDto getDateilByShippingNo(String shippingNo);
    boolean addReq(ShippingInfoDto req);
    /**
     * ä»“储物流出库自动生成发货单:直接置为已发货,不发起发货审批,内部扣减库存
     *
     * @return ç”Ÿæˆçš„发货台账id
     */
    Long addShippedFromOutbound(ShippingInfoDto req);
    /**
     * å‡ºåº“单被删除时冲销发货单:删除发货单与明细,并回滚已扣减的库存
     */
    void cancelByOutbound(Long shippingInfoId);
}
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -36,6 +36,7 @@
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
/**
@@ -171,7 +172,6 @@
    @Override
    public boolean addReq(ShippingInfoDto req) {
        // æ ¡éªŒè¯¥äº§å“æ˜¯å¦å·²æœ‰å¾…审核/审核中的发货记录,防止重复提交
        Long salesLedgerProductId = req.getSalesLedgerProductId();
        if (salesLedgerProductId != null) {
@@ -216,4 +216,31 @@
        this.updateById(req);
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long addShippedFromOutbound(ShippingInfoDto req) {
        // ä»“储物流出库即发货:不发起发货审批,直接落为已发货,库存由 add å†…部扣减
        req.setShippingNo(OrderUtils.countTodayByCreateTime(
                shippingInfoMapper, "SH", "shipping_no", LocalDateTime.now()));
        req.setStatus("已发货");
        if (req.getShippingDate() == null) {
            req.setShippingDate(new Date());
        }
        this.add(req);
        return req.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelByOutbound(Long shippingInfoId) {
        if (shippingInfoId == null) {
            return;
        }
        // å›žæ»šè¯¥å‘货单占用的库存
        stockUtils.deleteStockOutRecord(shippingInfoId, StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
        shippingProductDetailMapper.delete(new LambdaQueryWrapper<ShippingProductDetail>()
                .eq(ShippingProductDetail::getShippingInfoId, shippingInfoId));
        this.removeById(shippingInfoId);
    }
}
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java
@@ -27,6 +27,9 @@
    @Schema(description = "收款金额")
    private BigDecimal receiptPaymentAmount;
    @Schema(description = "付款金额")
    private BigDecimal paymentAmount;
    @Schema(description = "应收金额")
    private BigDecimal receiptableAmount;
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java
@@ -23,6 +23,10 @@
    //该客户销售收款累计金额
    private BigDecimal receiptPaymentAmount;
    @Schema(description = "付款金额")
    //该客户往来付款累计金额(payment_record æ‰‹å·¥æµæ°´ï¼‰
    private BigDecimal paymentAmount;
    @Schema(description = "应收金额")
    //该客户销售应收累计金额=初始金额-回款-退款
    private BigDecimal receiptableAmount;
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java
@@ -19,8 +19,6 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
/**
 * <p>
@@ -116,9 +114,8 @@
    @Log(title = "下载库存导入模板", businessType = BusinessType.EXPORT)
    @PostMapping("/downloadStockInventory")
    public void downloadStockInventory(HttpServletResponse response) {
        List<StockInventoryExportData> list = new ArrayList<>();
        ExcelUtil<StockInventoryExportData> util = new ExcelUtil<>(StockInventoryExportData.class);
        util.exportExcel(response, list, "库存模板");
        util.importTemplateExcel(response, "库存模板");
    }
    @PostMapping("/exportStockInventory")
src/main/java/com/ruoyi/stock/execl/StockInventoryExportData.java
@@ -1,6 +1,7 @@
package com.ruoyi.stock.execl;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import com.ruoyi.framework.aspectj.lang.annotation.Excel.Type;
import lombok.Data;
import java.math.BigDecimal;
@@ -40,6 +41,12 @@
    @Excel(name = "备注")
    private String remark;
    /**
     * å‚¨ç½å·ï¼ˆå‚¨ç½åˆ†ç±»ï¼‰ã€‚限定 EXPORT,避免污染导入模板
     */
    @Excel(name = "储罐号", type = Type.EXPORT)
    private String tankNo;
//
//    @Excel(name = "最新更新时间")
//    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
src/main/resources/mapper/basic/CustomerMapper.xml
@@ -128,10 +128,12 @@
               c.customer_name,
               T1.contractAmounts AS contractAmounts,
               IFNULL(T2.receiptPaymentAmount, 0) AS receiptPaymentAmount,
               IFNULL(T5.paymentAmount, 0) AS paymentAmount,
               T1.contractAmounts - IFNULL(T2.receiptPaymentAmount, 0) - IFNULL(T4.returnAmount, 0) AS receiptableAmount,
               IFNULL(T4.returnAmount, 0) AS returnAmount
        from (select customer_id, sum(contract_amount) as contractAmounts from sales_ledger group by customer_id) T1
        left join (select customer_id, sum(collection_amount) as receiptPaymentAmount from account_sales_collection group by customer_id) T2 on T1.customer_id = T2.customer_id
        left join (select party_id, sum(amount) as paymentAmount from payment_record where party_type = 1 and direction = 1 group by party_id) T5 on T5.party_id = T1.customer_id
        left join (
            select
                sl.customer_id,
@@ -157,6 +159,7 @@
               sl.execution_date,
               sl.contract_amount AS contract_amount,
               IFNULL(T1.receiptPaymentAmount, 0) AS receiptPaymentAmount,
               IFNULL(T5.paymentAmount, 0) AS paymentAmount,
               sl.contract_amount - IFNULL(T1.receiptPaymentAmount, 0) - IFNULL(T3.returnAmount, 0) AS receiptableAmount,
               IFNULL(T3.returnAmount, 0) AS returnAmount
        from sales_ledger sl
@@ -181,6 +184,12 @@
            where rm.status=1
            group by sl.id
        )T3 on T3.id = sl.id
        left join (
            select party_id, sum(amount) as paymentAmount
            from payment_record
            where party_type = 1 and direction = 1
            group by party_id
        )T5 on T5.party_id = sl.customer_id
        where sl.customer_id = #{customerId}
        order by sl.id desc
    </select>
src/main/resources/mapper/basic/SupplierManageMapper.xml
@@ -74,10 +74,12 @@
               sm.supplier_name,
               T1.contractAmounts,
               IFNULL(T2.paymentAmount, 0) AS paymentAmount,
               IFNULL(T5.receiptAmount, 0) AS receiptAmount,
               T1.contractAmounts - IFNULL(T2.paymentAmount, 0) - IFNULL(T4.returnAmount, 0) AS payableAmount,
               IFNULL(T4.returnAmount, 0) AS returnAmount
        from (select supplier_id, sum(contract_amount) as contractAmounts from purchase_ledger group by supplier_id) T1
        left join (select supplier_id, sum(payment_amount) as paymentAmount from account_purchase_payment group by supplier_id) T2 on T1.supplier_id = T2.supplier_id
        left join (select party_id, sum(amount) as receiptAmount from payment_record where party_type = 2 and direction = 2 group by party_id) T5 on T5.party_id = T1.supplier_id
        left join (
            select
                supplier_id,
@@ -100,6 +102,7 @@
              pl.execution_date,
              pl.contract_amount AS contract_amount,
              IFNULL(T1.paymentAmount, 0) AS paymentAmount,
              IFNULL(T5.receiptAmount, 0) AS receiptAmount,
              pl.contract_amount - IFNULL(T1.paymentAmount, 0) - IFNULL(T3.returnAmount, 0) AS payableAmount,
              IFNULL(T3.returnAmount, 0) AS returnAmount
       from purchase_ledger pl
@@ -126,6 +129,12 @@
                    left join purchase_ledger pl on pro.purchase_ledger_id = pl.id
           group by pl.id
       )T3 on T3.id = pl.id
       left join (
           select party_id, sum(amount) as receiptAmount
           from payment_record
           where party_type = 2 and direction = 2
           group by party_id
       )T5 on T5.party_id = pl.supplier_id
       where pl.supplier_id = #{supplierId}
       order by pl.id desc
    </select>
src/main/resources/mapper/measuringinstrumentledger/SparePartsRecordMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.measuringinstrumentledger.mapper.SparePartsRecordMapper">
    <select id="listPage" resultType="com.ruoyi.measuringinstrumentledger.pojo.SparePartsRecord">
        select t1.id,
               t1.spare_parts_id,
               t2.name as sparePartsName,
               t1.direction,
               t1.quantity,
               t1.source_type,
               t1.source_id,
               t1.source_no,
               t1.remark,
               t1.create_user,
               t1.create_time
        from spare_parts_record t1
                 left join spare_parts t2 on t2.id = t1.spare_parts_id
        <where>
            <if test="req.sparePartsId != null">
                and t1.spare_parts_id = #{req.sparePartsId}
            </if>
            <if test="req.direction != null">
                and t1.direction = #{req.direction}
            </if>
            <if test="req.sourceType != null">
                and t1.source_type = #{req.sourceType}
            </if>
        </where>
        order by t1.id desc
    </select>
</mapper>
src/main/resources/mapper/procurementrecord/ProcurementRecordMapper.xml
@@ -20,12 +20,19 @@
            t2.tax_exclusive_total_price,
            t3.inbound_num as quantityStock
        from  purchase_ledger t1
        left join sales_ledger_product t2 on t1.id = t2.sales_ledger_id and slp.type = 2
        left join sales_ledger_product t2 on t1.id = t2.sales_ledger_id and t2.type = 2
        left join procurement_record_storage t3 on t2.id = t3.sales_ledger_product_id
        where t1.purchase_contract_number = #{req.purchaseContractNumber}
        <if test="req.id != null and req.id != ''">
            and t3.id = #{req.id}
        </if>
        <where>
            <if test="req.purchaseLedgerId != null">
                and t1.id = #{req.purchaseLedgerId}
            </if>
            <if test="req.purchaseContractNumber != null and req.purchaseContractNumber != ''">
                and t1.purchase_contract_number = #{req.purchaseContractNumber}
            </if>
            <if test="req.id != null and req.id != ''">
                and t3.id = #{req.id}
            </if>
        </where>
        group by t2.id
    </select>
    <select id="listPage" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDto">
src/main/resources/mapper/procurementrecord/ProcurementRecordOutMapper.xml
@@ -105,6 +105,8 @@
        t2.product_category,
        t1.id,
        t1.code,
        t1.batch_no,
        t5.shipping_no,
        t2.specification_model,
        t2.unit,
        t2.tax_rate,
@@ -120,6 +122,7 @@
        left join sales_ledger_product t2 on t2.id = t1.sales_ledger_product_id and t2.type = 1
        left join sales_ledger t3 on t3.id = t2.sales_ledger_id
        left join procurement_record_storage t4 on t4.id = t1.procurement_record_storage_id
        left join shipping_info t5 on t5.id = t1.shipping_info_id
        <where>
            and t1.type = 2
            <if test="req.customerName != null and req.customerName != ''">
@@ -180,6 +183,18 @@
        order by id desc
        limit 1
    </select>
    <select id="listBatchNoByProductModelId" resultType="com.ruoyi.procurementrecord.bean.vo.BatchNoOptionVo">
        select si.product_model_id,
               si.batch_no,
               (si.qualitity - ifnull(si.locked_quantity, 0)) as availableQuantity
        from stock_inventory si
        where si.product_model_id = #{productModelId}
          and si.batch_no is not null
          and si.batch_no != ''
          and (si.qualitity - ifnull(si.locked_quantity, 0)) > 0
        order by si.batch_no
    </select>
    <select id="listPageBySemiProduct" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto">
        select
        t1.id,
src/main/resources/mapper/stock/StockInventoryMapper.xml
@@ -122,6 +122,13 @@
        product_id,
        'combined' as stockType,
        (
        select sir.tank_no
        from stock_in_record sir
        where sir.product_model_id = combined.product_model_id
        order by sir.id desc
        limit 1
        ) as tank_no,
        (
        select sir.oil_product
        from stock_in_record sir
        where sir.product_model_id = combined.product_model_id
@@ -336,7 +343,15 @@
            unit,
            product_name,
            product_id,
            'combined' as stockType
            'combined' as stockType,
            (
            select sir.tank_no
            from stock_in_record sir
            where sir.product_model_id = combined.product_model_id
            and (sir.batch_no = combined.batch_no or (combined.batch_no is null and sir.batch_no is null))
            order by sir.id desc
            limit 1
            ) as tank_no
        from (
            select
            si.batch_no,
@@ -1126,7 +1141,7 @@
        product_name,
        product_id
        order by
        batch_no
        MAX(update_time) desc
    </select>
</mapper>