tuqin
14 小时以前 55011a9cd84676913b62b3c130257939c1df2488
feat(生产核算): 新增成品/白盒部门产量工时折算核算

- 白盒部门:折算时长 = 月产量 × 标准数量 × 8(按 工序+型号/零件)
- 成品部门:月产折合小提琴 = 小提琴 + 大提琴×折算率 + 贝斯×折算率;折算时长 = 折合小提琴 ÷ 标准数量 × 8(按 工序)
- 月时间合计 = 折算时长求和;折合倍数 = 月时间合计 ÷(当月工作日 × 8)
- 新增配置:部门类型、标准数量、折算率(工序+员工)、月度工作日
- 提供汇总/明细查询、配置 CRUD、导出接口
- 新增建表与初始化脚本 sql/production_costing_config.sql
已添加22个文件
1381 ■■■■■ 文件已修改
sql/production_costing_config.sql 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionCostingQueryDto.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionCostingDetailVo.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionCostingRawVo.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionCostingSummaryVo.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionCostingController.java 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionCostingDeptMapper.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionCostingFactorMapper.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionCostingMapper.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionCostingStandardMapper.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionCostingWorkdayMapper.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionCostingDept.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionCostingFactor.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionCostingStandard.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionCostingWorkday.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionCostingService.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionCostingServiceImpl.java 377 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionCostingDeptMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionCostingFactorMapper.xml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionCostingMapper.xml 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionCostingStandardMapper.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionCostingWorkdayMapper.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/production_costing_config.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
-- ============================================================
-- ç”Ÿäº§æ ¸ç®—(成品部门 / ç™½ç›’部门)配置与结构补充脚本
-- æ•°æ®åº“:product-inventory-management-new-pro
-- è¯´æ˜Žï¼šæœ¬åœ°åº“结构落后于代码,production_product_main ç¼ºå°‘
--       æŠ¥å·¥ç»“æžœ/审核相关列,这里补齐;并新增核算配置表与演示数据。
-- ============================================================
SET NAMES utf8mb4;
-- ------------------------------------------------------------
-- 1. è¡¥å…… production_product_main ç¼ºå¤±åˆ—(报工结果 / å®¡æ ¸ / è¡¥ä¿®ç†å·¥æ—¶ï¼‰
-- ------------------------------------------------------------
ALTER TABLE `production_product_main`
    ADD COLUMN `report_type`     int          NULL COMMENT '报工结果类型:0合格,1轻微返工,2严重返工,3报废' AFTER `status`,
    ADD COLUMN `audit_status`    int          NULL DEFAULT 1 COMMENT '审核状态:0待审核,1审核通过,2审核不通过' AFTER `report_type`,
    ADD COLUMN `audit_remark`    varchar(255) NULL COMMENT '审核意见' AFTER `audit_status`,
    ADD COLUMN `repair_work_hour` decimal(10,2) NULL DEFAULT 0 COMMENT '补修理工时' AFTER `work_hour`;
-- å·²å­˜åœ¨æŠ¥å·¥æ•°æ®é»˜è®¤è§†ä¸ºï¼šå®¡æ ¸é€šè¿‡ã€åˆæ ¼
UPDATE `production_product_main` SET `report_type` = 0 WHERE `report_type` IS NULL;
UPDATE `production_product_main` SET `audit_status` = 1 WHERE `audit_status` IS NULL;
-- ------------------------------------------------------------
-- 2. æ–°å¢žæ ¸ç®—配置表
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `production_costing_dept`;
CREATE TABLE `production_costing_dept` (
    `id`          bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
    `dept_id`     bigint       NULL COMMENT '部门ID(sys_dept.dept_id)',
    `dept_type`   int          NULL COMMENT '部门类型:1成品部门,2白盒部门',
    `remark`      varchar(255) NULL COMMENT '备注',
    `create_user` bigint       NULL COMMENT '创建人',
    `create_time` datetime     NULL COMMENT '创建时间',
    `update_user` bigint       NULL COMMENT '修改人',
    `update_time` datetime     NULL COMMENT '修改时间',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '生产核算-部门类型配置';
DROP TABLE IF EXISTS `production_costing_standard`;
CREATE TABLE `production_costing_standard` (
    `id`                bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
    `dept_type`         int           NULL COMMENT '部门类型:1成品,2白盒',
    `process_id`        bigint        NULL COMMENT '工序ID(technology_operation.id)',
    `product_model_id`  bigint        NULL COMMENT '型号ID(product_model.id),白盒必填',
    `standard_quantity` decimal(18,4) NULL COMMENT '标准数量',
    `unit`              varchar(50)   NULL COMMENT '单位',
    `remark`            varchar(255)  NULL COMMENT '备注',
    `create_user`       bigint        NULL COMMENT '创建人',
    `create_time`       datetime      NULL COMMENT '创建时间',
    `update_user`       bigint        NULL COMMENT '修改人',
    `update_time`       datetime      NULL COMMENT '修改时间',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '生产核算-标准数量配置';
DROP TABLE IF EXISTS `production_costing_factor`;
CREATE TABLE `production_costing_factor` (
    `id`            bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
    `process_id`    bigint        NULL COMMENT '工序ID(technology_operation.id)',
    `staff_id`      bigint        NULL COMMENT '员工ID(sys_user.user_id)',
    `staff_name`    varchar(255)  NULL COMMENT '员工姓名',
    `violin_factor` decimal(18,4) NULL DEFAULT 1 COMMENT '小提琴折算率(基准)',
    `viola_factor`  decimal(18,4) NULL COMMENT '大提琴折算率',
    `bass_factor`   decimal(18,4) NULL COMMENT '贝斯折算率',
    `remark`        varchar(255)  NULL COMMENT '备注',
    `create_user`   bigint        NULL COMMENT '创建人',
    `create_time`   datetime      NULL COMMENT '创建时间',
    `update_user`   bigint        NULL COMMENT '修改人',
    `update_time`   datetime      NULL COMMENT '修改时间',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '生产核算-折算率配置(工序+员工)';
-- ------------------------------------------------------------
-- 3. æ¼”示数据(便于本地查看效果)
-- ------------------------------------------------------------
-- 3.1 æ–°å¢žéƒ¨é—¨ï¼šæˆå“éƒ¨é—¨ / ç™½ç›’部门
INSERT INTO `sys_dept` (`dept_id`, `parent_id`, `ancestors`, `dept_name`, `order_num`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES (220, 100, '0,100', '成品部门', 1, '0', '0', 'admin', NOW()),
       (221, 100, '0,100', '白盒部门', 2, '0', '0', 'admin', NOW());
-- 3.2 æŠŠæ¼”示工序归入对应部门(成品:冷工艺27/硫化28;白盒:收线25/切割2)
UPDATE `technology_operation` SET `dept_id` = 220 WHERE `id` IN (27, 28);
UPDATE `technology_operation` SET `dept_id` = 221 WHERE `id` IN (2, 25);
-- 3.3 éƒ¨é—¨ç±»åž‹é…ç½®
INSERT INTO `production_costing_dept` (`dept_id`, `dept_type`, `remark`, `create_time`)
VALUES (220, 1, '成品部门', NOW()),
       (221, 2, '白盒部门', NOW());
-- 3.4 æ ‡å‡†æ•°é‡é…ç½®
--     æˆå“ï¼šæŒ‰å·¥åº
--     ç™½ç›’:按工序 + åž‹å·
INSERT INTO `production_costing_standard` (`dept_type`, `process_id`, `product_model_id`, `standard_quantity`, `unit`, `remark`, `create_time`)
VALUES (1, 27, NULL, 10, 'm', '冷工艺标准数量', NOW()),
       (1, 28, NULL, 20, 'm', '硫化标准数量', NOW()),
       (2, 25, 251, 5, 'kg/km', '收线-铝合金线', NOW()),
       (2, 25, 256, 2, 'm', '收线-铝合金绞线', NOW()),
       (2, 2, 246, 4, 'kg/km', '切割-铝合金线', NOW());
-- 3.5 æŠ˜ç®—率配置(工序 + å‘˜å·¥ï¼‰
INSERT INTO `production_costing_factor` (`process_id`, `staff_id`, `staff_name`, `violin_factor`, `viola_factor`, `bass_factor`, `remark`, `create_time`)
VALUES (27, 1, '管理员账号', 1, 3, 6, '冷工艺-折算率', NOW()),
       (28, 1, '管理员账号', 1, 2, 4, '硫化-折算率', NOW());
-- 3.6 ä¹å™¨æ¼”示数据(小提琴/大提琴/贝斯),用于展示成品部门折算效果
INSERT INTO `product` (`id`, `parent_id`, `product_name`, `create_user`, `dept_id`)
VALUES (510, 276, '小提琴', 1, 220),
       (511, 276, '大提琴', 1, 220),
       (512, 276, '贝斯', 1, 220);
INSERT INTO `product_model` (`id`, `product_id`, `model`, `unit`, `create_user`, `dept_id`)
VALUES (5001, 510, 'VIOLIN-STD', '把', 1, 220),
       (5002, 511, 'CELLO-STD', '把', 1, 220),
       (5003, 512, 'BASS-STD', '把', 1, 220);
-- æŠ¥å·¥é“¾ï¼šè®¢å• -> å·¥è‰ºè·¯çº¿å·¥åº -> å·¥å• -> æŠ¥å·¥ -> äº§å‡º
INSERT INTO `production_order` (`id`, `product_model_id`, `technology_routing_id`, `nps_no`, `quantity`, `create_user`, `dept_id`)
VALUES (5000, 5001, 0, 'DEMO-ORDER-1', 100, 1, 220);
INSERT INTO `production_order_routing_operation` (`id`, `production_order_id`, `technology_routing_operation_id`, `technology_operation_id`, `operation_name`, `product_model_id`, `type`, `create_user`, `dept_id`)
VALUES (5000, 5000, 0, 27, '冷工艺', 5001, 1, 1, 220);
INSERT INTO `production_operation_task` (`id`, `production_order_routing_operation_id`, `technology_routing_operation_id`, `work_order_no`, `status`, `production_order_id`, `create_user`, `dept_id`)
VALUES (5000, 5000, 0, 'DEMO-WO-1', 4, 5000, 1, 220);
INSERT INTO `production_product_main` (`id`, `product_no`, `user_id`, `user_name`, `production_operation_task_id`, `status`, `dept_id`, `create_time`, `create_user`, `report_type`, `audit_status`)
VALUES (5000, 'BG-DEMO-001', 1, '管理员账号', 5000, 4, 220, '2026-09-15 10:00:00', 1, 0, 1);
INSERT INTO `production_product_output` (`production_product_main_id`, `product_model_id`, `quantity`, `scrap_qty`, `create_user`, `dept_id`)
VALUES (5000, 5001, 20, 0, 1, 220),
       (5000, 5002, 10, 0, 1, 220),
       (5000, 5003, 5, 0, 1, 220);
src/main/java/com/ruoyi/production/bean/dto/ProductionCostingQueryDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.production.bean.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
 * ç”Ÿäº§æ ¸ç®—查询参数
 */
@Data
@Schema(name = "ProductionCostingQueryDto", description = "生产核算查询参数")
public class ProductionCostingQueryDto implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "部门类型:1成品部门,2白盒部门")
    private Integer deptType;
    @Schema(description = "月份 yyyy-MM")
    private String yearMonth;
    @Schema(description = "员工姓名(模糊)")
    private String staffName;
    @Schema(description = "工序ID")
    private Long processId;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionCostingDetailVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package com.ruoyi.production.bean.vo;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * ç”Ÿäº§æ ¸ç®—明细分页行
 * ç™½ç›’:员工 + å·¥åº + åž‹å·/零件
 * æˆå“ï¼šå‘˜å·¥ + å·¥åºï¼ˆå«ä¸‰ç§ä¹å™¨æ•°é‡ã€æŠ˜ç®—率、折合小提琴)
 */
@Data
@Schema(name = "ProductionCostingDetailVo", description = "生产核算明细行")
public class ProductionCostingDetailVo implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long staffId;
    @Excel(name = "生产人")
    private String staffName;
    private String yearMonth;
    private Long processId;
    @Excel(name = "工序")
    private String processName;
    private Integer deptType;
    private Long productModelId;
    @Excel(name = "产品大类")
    private String productName;
    @Excel(name = "型号/零件")
    private String modelName;
    @Excel(name = "月产量")
    private BigDecimal quantity;
    @Excel(name = "小提琴数量")
    private BigDecimal violinQty;
    @Excel(name = "大提琴数量")
    private BigDecimal celloQty;
    @Excel(name = "贝斯数量")
    private BigDecimal bassQty;
    @Excel(name = "小提琴折算率")
    private BigDecimal violinFactor;
    @Excel(name = "大提琴折算率")
    private BigDecimal celloFactor;
    @Excel(name = "贝斯折算率")
    private BigDecimal bassFactor;
    @Excel(name = "月产折合小提琴")
    private BigDecimal foldedQuantity;
    @Excel(name = "标准数量")
    private BigDecimal standardQuantity;
    @Excel(name = "折算时长")
    private BigDecimal convertedHours;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionCostingRawVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.production.bean.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * ç”Ÿäº§æ ¸ç®—-报工聚合原始行(员工 + æœˆä»½ + å·¥åº + åž‹å·/产品大类)
 */
@Data
@Schema(name = "ProductionCostingRawVo", description = "生产核算原始聚合行")
public class ProductionCostingRawVo implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long staffId;
    private String staffName;
    private String yearMonth;
    private Long processId;
    private String processName;
    private Integer deptType;
    private Long productModelId;
    /** äº§å“å¤§ç±»ï¼ˆproduct.product_name),乐器判断依据 */
    private String productName;
    /** åž‹å·/规格(product_model.model) */
    private String modelName;
    /** æœˆäº§é‡ */
    private BigDecimal quantity;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionCostingSummaryVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package com.ruoyi.production.bean.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * ç”Ÿäº§æ ¸ç®—-汇总台账行(按员工)
 */
@Data
@Schema(name = "ProductionCostingSummaryVo", description = "生产核算汇总行")
public class ProductionCostingSummaryVo implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long staffId;
    private String staffName;
    private String yearMonth;
    /** äº§é‡ */
    private BigDecimal output;
    /** æœˆæ—¶é—´åˆè®¡ï¼ˆæŠ˜ç®—时长求和) */
    private BigDecimal monthTimeTotal;
    /** æŠ˜åˆå€æ•° = æœˆæ—¶é—´åˆè®¡ / (当月工作日 * 8) */
    private BigDecimal conversionRatio;
    /** å½“月工作日 */
    private Integer workDays;
    /** å½“月满勤工时 = å·¥ä½œæ—¥ * 8 */
    private BigDecimal monthHours;
}
src/main/java/com/ruoyi/production/controller/ProductionCostingController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,185 @@
package com.ruoyi.production.controller;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.ProductionCostingQueryDto;
import com.ruoyi.production.bean.vo.ProductionCostingDetailVo;
import com.ruoyi.production.bean.vo.ProductionCostingSummaryVo;
import com.ruoyi.production.pojo.ProductionCostingDept;
import com.ruoyi.production.pojo.ProductionCostingFactor;
import com.ruoyi.production.pojo.ProductionCostingStandard;
import com.ruoyi.production.pojo.ProductionCostingWorkday;
import com.ruoyi.production.service.ProductionCostingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
 * ç”Ÿäº§æ ¸ç®—(成品部门 / ç™½ç›’部门)前端控制器
 */
@RestController
@RequestMapping("/productionCosting")
@RequiredArgsConstructor
@Tag(name = "生产核算")
public class ProductionCostingController {
    private final ProductionCostingService productionCostingService;
    @GetMapping("/summary")
    @Log(title = "生产核算-汇总查询", businessType = BusinessType.OTHER)
    @Operation(summary = "生产核算按员工汇总")
    public R<List<ProductionCostingSummaryVo>> summary(ProductionCostingQueryDto dto) {
        return R.ok(productionCostingService.summary(dto));
    }
    @GetMapping("/detail")
    @Log(title = "生产核算-明细查询", businessType = BusinessType.OTHER)
    @Operation(summary = "生产核算明细查询")
    public R<List<ProductionCostingDetailVo>> detail(ProductionCostingQueryDto dto) {
        return R.ok(productionCostingService.detailList(dto));
    }
    @GetMapping("/workday")
    @Operation(summary = "查询月份工作日信息")
    public R<Map<String, Object>> workday(String yearMonth) {
        return R.ok(productionCostingService.workdayInfo(yearMonth));
    }
    @PostMapping("/export")
    @Log(title = "生产核算-导出", businessType = BusinessType.EXPORT)
    @Operation(summary = "导出生产核算明细")
    public void export(HttpServletResponse response, ProductionCostingQueryDto dto) {
        ExcelUtil<ProductionCostingDetailVo> util = new ExcelUtil<>(ProductionCostingDetailVo.class);
        util.exportExcel(response, productionCostingService.detailList(dto), "生产核算明细");
    }
    /* ---------------- éƒ¨é—¨ç±»åž‹é…ç½® ---------------- */
    @GetMapping("/dept/list")
    @Operation(summary = "部门类型配置列表")
    public R<List<ProductionCostingDept>> deptList() {
        return R.ok(productionCostingService.listDept());
    }
    @PostMapping("/dept")
    @Log(title = "生产核算-部门类型配置新增", businessType = BusinessType.INSERT)
    @Operation(summary = "新增部门类型配置")
    public R<Boolean> deptAdd(@RequestBody ProductionCostingDept dept) {
        return R.ok(productionCostingService.saveDept(dept));
    }
    @PutMapping("/dept")
    @Log(title = "生产核算-部门类型配置修改", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改部门类型配置")
    public R<Boolean> deptEdit(@RequestBody ProductionCostingDept dept) {
        return R.ok(productionCostingService.updateDept(dept));
    }
    @DeleteMapping("/dept/{id}")
    @Log(title = "生产核算-部门类型配置删除", businessType = BusinessType.DELETE)
    @Operation(summary = "删除部门类型配置")
    public R<Boolean> deptRemove(@PathVariable Long id) {
        return R.ok(productionCostingService.removeDept(id));
    }
    /* ---------------- æ ‡å‡†æ•°é‡é…ç½® ---------------- */
    @GetMapping("/standard/list")
    @Operation(summary = "标准数量配置列表")
    public R<List<ProductionCostingStandard>> standardList(ProductionCostingStandard query) {
        return R.ok(productionCostingService.listStandard(query));
    }
    @PostMapping("/standard")
    @Log(title = "生产核算-标准数量配置新增", businessType = BusinessType.INSERT)
    @Operation(summary = "新增标准数量配置")
    public R<Boolean> standardAdd(@RequestBody ProductionCostingStandard standard) {
        return R.ok(productionCostingService.saveStandard(standard));
    }
    @PutMapping("/standard")
    @Log(title = "生产核算-标准数量配置修改", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改标准数量配置")
    public R<Boolean> standardEdit(@RequestBody ProductionCostingStandard standard) {
        return R.ok(productionCostingService.updateStandard(standard));
    }
    @DeleteMapping("/standard/{id}")
    @Log(title = "生产核算-标准数量配置删除", businessType = BusinessType.DELETE)
    @Operation(summary = "删除标准数量配置")
    public R<Boolean> standardRemove(@PathVariable Long id) {
        return R.ok(productionCostingService.removeStandard(id));
    }
    /* ---------------- æŠ˜ç®—率配置 ---------------- */
    @GetMapping("/factor/list")
    @Operation(summary = "折算率配置列表")
    public R<List<ProductionCostingFactor>> factorList(ProductionCostingFactor query) {
        return R.ok(productionCostingService.listFactor(query));
    }
    @PostMapping("/factor")
    @Log(title = "生产核算-折算率配置新增", businessType = BusinessType.INSERT)
    @Operation(summary = "新增折算率配置")
    public R<Boolean> factorAdd(@RequestBody ProductionCostingFactor factor) {
        return R.ok(productionCostingService.saveFactor(factor));
    }
    @PutMapping("/factor")
    @Log(title = "生产核算-折算率配置修改", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改折算率配置")
    public R<Boolean> factorEdit(@RequestBody ProductionCostingFactor factor) {
        return R.ok(productionCostingService.updateFactor(factor));
    }
    @DeleteMapping("/factor/{id}")
    @Log(title = "生产核算-折算率配置删除", businessType = BusinessType.DELETE)
    @Operation(summary = "删除折算率配置")
    public R<Boolean> factorRemove(@PathVariable Long id) {
        return R.ok(productionCostingService.removeFactor(id));
    }
    /* ---------------- æœˆåº¦å·¥ä½œæ—¥é…ç½® ---------------- */
    @GetMapping("/workdayConfig/list")
    @Operation(summary = "月度工作日配置列表")
    public R<List<ProductionCostingWorkday>> workdayList(ProductionCostingWorkday query) {
        return R.ok(productionCostingService.listWorkday(query));
    }
    @PostMapping("/workdayConfig")
    @Log(title = "生产核算-月度工作日配置新增", businessType = BusinessType.INSERT)
    @Operation(summary = "新增月度工作日配置")
    public R<Boolean> workdayAdd(@RequestBody ProductionCostingWorkday workday) {
        return R.ok(productionCostingService.saveWorkday(workday));
    }
    @PutMapping("/workdayConfig")
    @Log(title = "生产核算-月度工作日配置修改", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改月度工作日配置")
    public R<Boolean> workdayEdit(@RequestBody ProductionCostingWorkday workday) {
        return R.ok(productionCostingService.updateWorkday(workday));
    }
    @DeleteMapping("/workdayConfig/{id}")
    @Log(title = "生产核算-月度工作日配置删除", businessType = BusinessType.DELETE)
    @Operation(summary = "删除月度工作日配置")
    public R<Boolean> workdayRemove(@PathVariable Long id) {
        return R.ok(productionCostingService.removeWorkday(id));
    }
}
src/main/java/com/ruoyi/production/mapper/ProductionCostingDeptMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.production.pojo.ProductionCostingDept;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * ç”Ÿäº§æ ¸ç®—-部门类型配置 Mapper
 */
@Mapper
public interface ProductionCostingDeptMapper extends BaseMapper<ProductionCostingDept> {
    /**
     * æŸ¥è¯¢éƒ¨é—¨ç±»åž‹é…ç½®ï¼ˆå«éƒ¨é—¨åç§°ï¼‰
     */
    List<ProductionCostingDept> selectDeptConfigs();
}
src/main/java/com/ruoyi/production/mapper/ProductionCostingFactorMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.production.pojo.ProductionCostingFactor;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * ç”Ÿäº§æ ¸ç®—-折算率配置 Mapper
 */
@Mapper
public interface ProductionCostingFactorMapper extends BaseMapper<ProductionCostingFactor> {
    /**
     * æŸ¥è¯¢æŠ˜ç®—率配置列表(含工序名称)
     */
    List<ProductionCostingFactor> selectFactorList(@Param("c") ProductionCostingFactor query);
}
src/main/java/com/ruoyi/production/mapper/ProductionCostingMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.production.mapper;
import com.ruoyi.production.bean.dto.ProductionCostingQueryDto;
import com.ruoyi.production.bean.vo.ProductionCostingRawVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * ç”Ÿäº§æ ¸ç®—-核算统计 Mapper
 */
@Mapper
public interface ProductionCostingMapper {
    /**
     * æŸ¥è¯¢æŸæ¡ä»¶ä¸‹çš„æŠ¥å·¥èšåˆåŽŸå§‹è¡Œï¼ˆå‘˜å·¥+月份+工序+型号/产品大类)
     */
    List<ProductionCostingRawVo> selectCostingRows(@Param("c") ProductionCostingQueryDto dto);
}
src/main/java/com/ruoyi/production/mapper/ProductionCostingStandardMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.production.pojo.ProductionCostingStandard;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * ç”Ÿäº§æ ¸ç®—-标准数量配置 Mapper
 */
@Mapper
public interface ProductionCostingStandardMapper extends BaseMapper<ProductionCostingStandard> {
    /**
     * æŸ¥è¯¢æ ‡å‡†æ•°é‡é…ç½®åˆ—表(含工序名称、型号名称)
     */
    List<ProductionCostingStandard> selectStandardList(@Param("c") ProductionCostingStandard query);
}
src/main/java/com/ruoyi/production/mapper/ProductionCostingWorkdayMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.production.pojo.ProductionCostingWorkday;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * ç”Ÿäº§æ ¸ç®—-月度工作日配置 Mapper
 */
@Mapper
public interface ProductionCostingWorkdayMapper extends BaseMapper<ProductionCostingWorkday> {
    /**
     * æŸ¥è¯¢æœˆåº¦å·¥ä½œæ—¥é…ç½®åˆ—表
     */
    List<ProductionCostingWorkday> selectWorkdayList(@Param("c") ProductionCostingWorkday query);
}
src/main/java/com/ruoyi/production/pojo/ProductionCostingDept.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package com.ruoyi.production.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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * ç”Ÿäº§æ ¸ç®—-部门类型配置(区分成品部门 / ç™½ç›’部门)
 */
@Data
@TableName("production_costing_dept")
@Schema(name = "ProductionCostingDept对象", description = "生产核算-部门类型配置")
public class ProductionCostingDept implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    @Schema(description = "主键")
    private Long id;
    @Schema(description = "部门ID")
    private Long deptId;
    @Schema(description = "部门类型:1成品部门,2白盒部门")
    private Integer deptType;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "部门名称(非数据库字段)")
    @TableField(exist = false)
    private String deptName;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
src/main/java/com/ruoyi/production/pojo/ProductionCostingFactor.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package com.ruoyi.production.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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * ç”Ÿäº§æ ¸ç®—-折算率配置(工序 + å‘˜å·¥ï¼‰
 * æˆå“éƒ¨é—¨ï¼šæ¯ä¸ªå·¥åºçš„æ¯ä¸ªå‘˜å·¥æŠ˜ç®—率可不同(小提琴为基准 1)
 */
@Data
@TableName("production_costing_factor")
@Schema(name = "ProductionCostingFactor对象", description = "生产核算-折算率配置")
public class ProductionCostingFactor implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    @Schema(description = "主键")
    private Long id;
    @Schema(description = "工序ID")
    private Long processId;
    @Schema(description = "员工ID")
    private Long staffId;
    @Schema(description = "员工姓名")
    private String staffName;
    @Schema(description = "小提琴折算率(基准)")
    private BigDecimal violinFactor;
    @Schema(description = "大提琴折算率")
    private BigDecimal violaFactor;
    @Schema(description = "贝斯折算率")
    private BigDecimal bassFactor;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "工序名称(非数据库字段)")
    @TableField(exist = false)
    private String processName;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
src/main/java/com/ruoyi/production/pojo/ProductionCostingStandard.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package com.ruoyi.production.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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * ç”Ÿäº§æ ¸ç®—-标准数量配置
 * æˆå“éƒ¨é—¨ï¼šæŒ‰å·¥åºé…ç½®ï¼ˆproduct_model_id å¯ç©ºï¼‰
 * ç™½ç›’部门:按 å·¥åº + åž‹å· é…ç½®
 */
@Data
@TableName("production_costing_standard")
@Schema(name = "ProductionCostingStandard对象", description = "生产核算-标准数量配置")
public class ProductionCostingStandard implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    @Schema(description = "主键")
    private Long id;
    @Schema(description = "部门类型:1成品,2白盒")
    private Integer deptType;
    @Schema(description = "工序ID")
    private Long processId;
    @Schema(description = "型号ID(白盒必填)")
    private Long productModelId;
    @Schema(description = "标准数量")
    private BigDecimal standardQuantity;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "工序名称(非数据库字段)")
    @TableField(exist = false)
    private String processName;
    @Schema(description = "型号名称(非数据库字段)")
    @TableField(exist = false)
    private String modelName;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
src/main/java/com/ruoyi/production/pojo/ProductionCostingWorkday.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package com.ruoyi.production.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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * ç”Ÿäº§æ ¸ç®—-月度工作日配置
 */
@Data
@TableName("production_costing_workday")
@Schema(name = "ProductionCostingWorkday对象", description = "生产核算-月度工作日配置")
public class ProductionCostingWorkday implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    @Schema(description = "主键")
    private Long id;
    @Schema(description = "年月 yyyy-MM")
    @TableField("work_month")
    private String yearMonth;
    @Schema(description = "当月工作日天数")
    private Integer workDays;
    @Schema(description = "备注")
    private String remark;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
src/main/java/com/ruoyi/production/service/ProductionCostingService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package com.ruoyi.production.service;
import com.ruoyi.production.bean.dto.ProductionCostingQueryDto;
import com.ruoyi.production.bean.vo.ProductionCostingDetailVo;
import com.ruoyi.production.bean.vo.ProductionCostingSummaryVo;
import com.ruoyi.production.pojo.ProductionCostingDept;
import com.ruoyi.production.pojo.ProductionCostingFactor;
import com.ruoyi.production.pojo.ProductionCostingStandard;
import com.ruoyi.production.pojo.ProductionCostingWorkday;
import java.util.List;
import java.util.Map;
/**
 * ç”Ÿäº§æ ¸ç®—(成品部门 / ç™½ç›’部门)服务
 */
public interface ProductionCostingService {
    /** æŒ‰å‘˜å·¥æ±‡æ€»ï¼šäº§é‡ã€æœˆæ—¶é—´åˆè®¡ã€æŠ˜åˆå€æ•° */
    List<ProductionCostingSummaryVo> summary(ProductionCostingQueryDto dto);
    /** æ˜Žç»†è¡Œï¼ˆç™½ç›’:工序+型号;成品:工序) */
    List<ProductionCostingDetailVo> detailList(ProductionCostingQueryDto dto);
    /** æœˆä»½å·¥ä½œæ—¥ä¿¡æ¯ */
    Map<String, Object> workdayInfo(String yearMonth);
    /* ---------------- éƒ¨é—¨ç±»åž‹é…ç½® ---------------- */
    List<ProductionCostingDept> listDept();
    boolean saveDept(ProductionCostingDept dept);
    boolean updateDept(ProductionCostingDept dept);
    boolean removeDept(Long id);
    /* ---------------- æ ‡å‡†æ•°é‡é…ç½® ---------------- */
    List<ProductionCostingStandard> listStandard(ProductionCostingStandard query);
    boolean saveStandard(ProductionCostingStandard standard);
    boolean updateStandard(ProductionCostingStandard standard);
    boolean removeStandard(Long id);
    /* ---------------- æŠ˜ç®—率配置 ---------------- */
    List<ProductionCostingFactor> listFactor(ProductionCostingFactor query);
    boolean saveFactor(ProductionCostingFactor factor);
    boolean updateFactor(ProductionCostingFactor factor);
    boolean removeFactor(Long id);
    /* ---------------- æœˆåº¦å·¥ä½œæ—¥é…ç½® ---------------- */
    List<ProductionCostingWorkday> listWorkday(ProductionCostingWorkday query);
    boolean saveWorkday(ProductionCostingWorkday workday);
    boolean updateWorkday(ProductionCostingWorkday workday);
    boolean removeWorkday(Long id);
}
src/main/java/com/ruoyi/production/service/impl/ProductionCostingServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,377 @@
package com.ruoyi.production.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.production.bean.dto.ProductionCostingQueryDto;
import com.ruoyi.production.bean.vo.ProductionCostingDetailVo;
import com.ruoyi.production.bean.vo.ProductionCostingRawVo;
import com.ruoyi.production.bean.vo.ProductionCostingSummaryVo;
import com.ruoyi.production.mapper.ProductionCostingDeptMapper;
import com.ruoyi.production.mapper.ProductionCostingFactorMapper;
import com.ruoyi.production.mapper.ProductionCostingMapper;
import com.ruoyi.production.mapper.ProductionCostingStandardMapper;
import com.ruoyi.production.mapper.ProductionCostingWorkdayMapper;
import com.ruoyi.production.pojo.ProductionCostingDept;
import com.ruoyi.production.pojo.ProductionCostingFactor;
import com.ruoyi.production.pojo.ProductionCostingStandard;
import com.ruoyi.production.pojo.ProductionCostingWorkday;
import com.ruoyi.production.service.ProductionCostingService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.DayOfWeek;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
 * ç”Ÿäº§æ ¸ç®—服务实现
 * <p>
 * è®¡ç®—规则(严格按需求):
 * ç™½ç›’部门:折算时长 = æœˆäº§é‡ Ã— æ ‡å‡†æ•°é‡ Ã— 8
 * æˆå“éƒ¨é—¨ï¼šæœˆäº§æŠ˜åˆå°æ = å°æç´ + å¤§æç´Ã—大提折算率 + è´æ–¯Ã—贝斯折算率
 * æŠ˜ç®—æ—¶é•¿ = æœˆäº§æŠ˜åˆå°æ Ã· æ ‡å‡†æ•°é‡ Ã— 8
 * æœˆæ—¶é—´åˆè®¡ = æŠ˜ç®—时长求和
 * æŠ˜åˆå€æ•° = æœˆæ—¶é—´åˆè®¡ Ã·ï¼ˆå½“月工作日 Ã— 8)
 */
@Service
@RequiredArgsConstructor
public class ProductionCostingServiceImpl implements ProductionCostingService {
    private static final BigDecimal EIGHT = new BigDecimal("8");
    private static final BigDecimal ONE = new BigDecimal("1");
    private static final String VIOLIN = "小提琴";
    private static final String CELLO = "大提琴";
    private static final String BASS = "贝斯";
    private final ProductionCostingMapper productionCostingMapper;
    private final ProductionCostingStandardMapper standardMapper;
    private final ProductionCostingFactorMapper factorMapper;
    private final ProductionCostingDeptMapper deptMapper;
    private final ProductionCostingWorkdayMapper workdayMapper;
    @Override
    public List<ProductionCostingDetailVo> detailList(ProductionCostingQueryDto dto) {
        ProductionCostingQueryDto query = dto == null ? new ProductionCostingQueryDto() : dto;
        List<ProductionCostingRawVo> rows = productionCostingMapper.selectCostingRows(query);
        // æ ‡å‡†æ•°é‡ï¼šæˆå“(1) æŒ‰å·¥åºï¼›ç™½ç›’(2) æŒ‰ å·¥åº+型号
        Map<Long, BigDecimal> standardByProcess = new HashMap<>();
        Map<String, BigDecimal> standardByProcessModel = new HashMap<>();
        List<ProductionCostingStandard> standardList = standardMapper.selectList(null);
        for (ProductionCostingStandard s : standardList) {
            if (s.getProcessId() == null || s.getStandardQuantity() == null) {
                continue;
            }
            if (Integer.valueOf(2).equals(s.getDeptType())) {
                standardByProcessModel.put(key(s.getProcessId(), s.getProductModelId()), s.getStandardQuantity());
            } else {
                standardByProcess.put(s.getProcessId(), s.getStandardQuantity());
            }
        }
        // æŠ˜ç®—率:工序 + å‘˜å·¥
        Map<String, ProductionCostingFactor> factorMap = new HashMap<>();
        for (ProductionCostingFactor f : factorMapper.selectList(null)) {
            factorMap.put(key(f.getProcessId(), f.getStaffId()), f);
        }
        List<ProductionCostingDetailVo> result = new ArrayList<>();
        // ---------- æˆå“éƒ¨é—¨ï¼šæŒ‰ å‘˜å·¥ + æœˆä»½ + å·¥åº èšåˆ ----------
        Map<String, ProductionCostingDetailVo> finishedGroup = new LinkedHashMap<>();
        // ---------- ç™½ç›’部门:按 å‘˜å·¥ + æœˆä»½ + å·¥åº + åž‹å· èšåˆ ----------
        Map<String, ProductionCostingDetailVo> whiteGroup = new LinkedHashMap<>();
        for (ProductionCostingRawVo row : rows) {
            BigDecimal qty = nz(row.getQuantity());
            boolean white = Integer.valueOf(2).equals(row.getDeptType());
            if (white) {
                String k = key(row.getStaffId(), row.getYearMonth(), row.getProcessId(), row.getProductModelId());
                ProductionCostingDetailVo vo = whiteGroup.get(k);
                if (vo == null) {
                    vo = new ProductionCostingDetailVo();
                    vo.setStaffId(row.getStaffId());
                    vo.setStaffName(row.getStaffName());
                    vo.setYearMonth(row.getYearMonth());
                    vo.setProcessId(row.getProcessId());
                    vo.setProcessName(row.getProcessName());
                    vo.setDeptType(row.getDeptType());
                    vo.setProductModelId(row.getProductModelId());
                    vo.setProductName(row.getProductName());
                    vo.setModelName(row.getModelName());
                    vo.setQuantity(BigDecimal.ZERO);
                    whiteGroup.put(k, vo);
                }
                vo.setQuantity(vo.getQuantity().add(qty));
            } else {
                String k = key(row.getStaffId(), row.getYearMonth(), row.getProcessId());
                ProductionCostingDetailVo vo = finishedGroup.get(k);
                if (vo == null) {
                    vo = new ProductionCostingDetailVo();
                    vo.setStaffId(row.getStaffId());
                    vo.setStaffName(row.getStaffName());
                    vo.setYearMonth(row.getYearMonth());
                    vo.setProcessId(row.getProcessId());
                    vo.setProcessName(row.getProcessName());
                    vo.setDeptType(row.getDeptType());
                    vo.setQuantity(BigDecimal.ZERO);
                    vo.setViolinQty(BigDecimal.ZERO);
                    vo.setCelloQty(BigDecimal.ZERO);
                    vo.setBassQty(BigDecimal.ZERO);
                    finishedGroup.put(k, vo);
                }
                vo.setQuantity(vo.getQuantity().add(qty));
                if (VIOLIN.equals(row.getProductName())) {
                    vo.setViolinQty(vo.getViolinQty().add(qty));
                } else if (CELLO.equals(row.getProductName())) {
                    vo.setCelloQty(vo.getCelloQty().add(qty));
                } else if (BASS.equals(row.getProductName())) {
                    vo.setBassQty(vo.getBassQty().add(qty));
                } else {
                    // éžä¹å™¨äº§å“é»˜è®¤æŒ‰å°æç´å½“量 1:1 è®¡å…¥
                    vo.setViolinQty(vo.getViolinQty().add(qty));
                }
            }
        }
        // æˆå“ï¼šæŠ˜ç®—
        for (ProductionCostingDetailVo vo : finishedGroup.values()) {
            ProductionCostingFactor factor = factorMap.get(key(vo.getProcessId(), vo.getStaffId()));
            BigDecimal violinFactor = factor == null ? ONE : nzDefault(factor.getViolinFactor(), ONE);
            BigDecimal celloFactor = factor == null ? ONE : nzDefault(factor.getViolaFactor(), ONE);
            BigDecimal bassFactor = factor == null ? ONE : nzDefault(factor.getBassFactor(), ONE);
            vo.setViolinFactor(violinFactor);
            vo.setCelloFactor(celloFactor);
            vo.setBassFactor(bassFactor);
            BigDecimal folded = nz(vo.getViolinQty()).multiply(violinFactor)
                    .add(nz(vo.getCelloQty()).multiply(celloFactor))
                    .add(nz(vo.getBassQty()).multiply(bassFactor))
                    .setScale(2, RoundingMode.HALF_UP);
            vo.setFoldedQuantity(folded);
            BigDecimal standard = standardByProcess.get(vo.getProcessId());
            vo.setStandardQuantity(standard);
            vo.setConvertedHours(calcFinishedHours(folded, standard));
        }
        // ç™½ç›’:折算时长 = æœˆäº§é‡ Ã— æ ‡å‡†æ•°é‡ Ã— 8
        for (ProductionCostingDetailVo vo : whiteGroup.values()) {
            BigDecimal standard = standardByProcessModel.get(key(vo.getProcessId(), vo.getProductModelId()));
            vo.setStandardQuantity(standard);
            BigDecimal hours = nz(standard).compareTo(BigDecimal.ZERO) <= 0
                    ? BigDecimal.ZERO
                    : nz(vo.getQuantity()).multiply(standard).multiply(EIGHT);
            vo.setConvertedHours(hours.setScale(2, RoundingMode.HALF_UP));
        }
        result.addAll(finishedGroup.values());
        result.addAll(whiteGroup.values());
        return result;
    }
    @Override
    public List<ProductionCostingSummaryVo> summary(ProductionCostingQueryDto dto) {
        ProductionCostingQueryDto query = dto == null ? new ProductionCostingQueryDto() : dto;
        String yearMonth = (query.getYearMonth() == null || query.getYearMonth().isEmpty())
                ? YearMonth.now().toString() : query.getYearMonth();
        query.setYearMonth(yearMonth);
        int workDays = resolveWorkDays(yearMonth);
        BigDecimal fullHours = new BigDecimal(workDays).multiply(EIGHT);
        List<ProductionCostingDetailVo> details = detailList(query);
        Map<Long, ProductionCostingSummaryVo> map = new LinkedHashMap<>();
        for (ProductionCostingDetailVo d : details) {
            ProductionCostingSummaryVo vo = map.get(d.getStaffId());
            if (vo == null) {
                vo = new ProductionCostingSummaryVo();
                vo.setStaffId(d.getStaffId());
                vo.setStaffName(d.getStaffName());
                vo.setYearMonth(yearMonth);
                vo.setOutput(BigDecimal.ZERO);
                vo.setMonthTimeTotal(BigDecimal.ZERO);
                vo.setWorkDays(workDays);
                vo.setMonthHours(fullHours);
                map.put(d.getStaffId(), vo);
            }
            vo.setOutput(vo.getOutput().add(nz(d.getQuantity())).setScale(2, RoundingMode.HALF_UP));
            vo.setMonthTimeTotal(vo.getMonthTimeTotal().add(nz(d.getConvertedHours())).setScale(2, RoundingMode.HALF_UP));
        }
        List<ProductionCostingSummaryVo> result = new ArrayList<>(map.values());
        for (ProductionCostingSummaryVo vo : result) {
            BigDecimal divisor = fullHours;
            BigDecimal ratio = divisor.compareTo(BigDecimal.ZERO) == 0
                    ? BigDecimal.ZERO
                    : vo.getMonthTimeTotal().divide(divisor, 2, RoundingMode.HALF_UP);
            vo.setConversionRatio(ratio);
        }
        return result;
    }
    @Override
    public Map<String, Object> workdayInfo(String yearMonth) {
        String ym = (yearMonth == null || yearMonth.isEmpty()) ? YearMonth.now().toString() : yearMonth;
        ProductionCostingWorkday cfg = findWorkday(ym);
        int workDays = (cfg != null && cfg.getWorkDays() != null) ? cfg.getWorkDays() : workDays(ym);
        Map<String, Object> map = new HashMap<>();
        map.put("yearMonth", ym);
        map.put("workDays", workDays);
        map.put("dayHours", 8);
        map.put("monthHours", new BigDecimal(workDays).multiply(EIGHT).setScale(2, RoundingMode.HALF_UP));
        map.put("source", (cfg != null && cfg.getWorkDays() != null) ? "CONFIG" : "DEFAULT");
        return map;
    }
    /* ================= é…ç½® CRUD ================= */
    @Override
    public List<ProductionCostingDept> listDept() {
        return deptMapper.selectDeptConfigs();
    }
    @Override
    public boolean saveDept(ProductionCostingDept dept) {
        return deptMapper.insert(dept) > 0;
    }
    @Override
    public boolean updateDept(ProductionCostingDept dept) {
        return deptMapper.updateById(dept) > 0;
    }
    @Override
    public boolean removeDept(Long id) {
        return deptMapper.deleteById(id) > 0;
    }
    @Override
    public List<ProductionCostingStandard> listStandard(ProductionCostingStandard query) {
        return standardMapper.selectStandardList(query == null ? new ProductionCostingStandard() : query);
    }
    @Override
    public boolean saveStandard(ProductionCostingStandard standard) {
        return standardMapper.insert(standard) > 0;
    }
    @Override
    public boolean updateStandard(ProductionCostingStandard standard) {
        return standardMapper.updateById(standard) > 0;
    }
    @Override
    public boolean removeStandard(Long id) {
        return standardMapper.deleteById(id) > 0;
    }
    @Override
    public List<ProductionCostingFactor> listFactor(ProductionCostingFactor query) {
        return factorMapper.selectFactorList(query == null ? new ProductionCostingFactor() : query);
    }
    @Override
    public boolean saveFactor(ProductionCostingFactor factor) {
        return factorMapper.insert(factor) > 0;
    }
    @Override
    public boolean updateFactor(ProductionCostingFactor factor) {
        return factorMapper.updateById(factor) > 0;
    }
    @Override
    public boolean removeFactor(Long id) {
        return factorMapper.deleteById(id) > 0;
    }
    @Override
    public List<ProductionCostingWorkday> listWorkday(ProductionCostingWorkday query) {
        return workdayMapper.selectWorkdayList(query == null ? new ProductionCostingWorkday() : query);
    }
    @Override
    public boolean saveWorkday(ProductionCostingWorkday workday) {
        return workdayMapper.insert(workday) > 0;
    }
    @Override
    public boolean updateWorkday(ProductionCostingWorkday workday) {
        return workdayMapper.updateById(workday) > 0;
    }
    @Override
    public boolean removeWorkday(Long id) {
        return workdayMapper.deleteById(id) > 0;
    }
    /* ================= å·¥å…·æ–¹æ³• ================= */
    /** æŸ¥æœˆåº¦å·¥ä½œæ—¥é…ç½® */
    private ProductionCostingWorkday findWorkday(String yearMonth) {
        if (yearMonth == null || yearMonth.isEmpty()) {
            return null;
        }
        return workdayMapper.selectOne(new LambdaQueryWrapper<ProductionCostingWorkday>()
                .eq(ProductionCostingWorkday::getYearMonth, yearMonth)
                .last("limit 1"));
    }
    /** å·¥ä½œæ—¥ï¼šä¼˜å…ˆå–月度配置,未配置则按周一~周五 */
    private int resolveWorkDays(String yearMonth) {
        ProductionCostingWorkday cfg = findWorkday(yearMonth);
        if (cfg != null && cfg.getWorkDays() != null) {
            return cfg.getWorkDays();
        }
        return workDays(yearMonth);
    }
    /** æˆå“æŠ˜ç®—:折合小提琴 / æ ‡å‡†æ•°é‡ * 8;标准数量<=0 è¿”回 0 */
    private BigDecimal calcFinishedHours(BigDecimal folded, BigDecimal standard) {
        if (standard == null || standard.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        return folded.divide(standard, 8, RoundingMode.HALF_UP)
                .multiply(EIGHT)
                .setScale(2, RoundingMode.HALF_UP);
    }
    private int workDays(String yearMonth) {
        try {
            YearMonth ym = YearMonth.parse(yearMonth);
            int count = 0;
            for (int d = 1; d <= ym.lengthOfMonth(); d++) {
                DayOfWeek dow = ym.atDay(d).getDayOfWeek();
                if (dow != DayOfWeek.SATURDAY && dow != DayOfWeek.SUNDAY) {
                    count++;
                }
            }
            return count;
        } catch (Exception e) {
            return 0;
        }
    }
    private BigDecimal nz(BigDecimal v) {
        return v == null ? BigDecimal.ZERO : v;
    }
    private BigDecimal nzDefault(BigDecimal v, BigDecimal def) {
        return v == null ? def : v;
    }
    private String key(Object... parts) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < parts.length; i++) {
            if (i > 0) {
                sb.append(':');
            }
            sb.append(parts[i] == null ? "null" : parts[i]);
        }
        return sb.toString();
    }
}
src/main/resources/mapper/production/ProductionCostingDeptMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
<?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.production.mapper.ProductionCostingDeptMapper">
    <select id="selectDeptConfigs" resultType="com.ruoyi.production.pojo.ProductionCostingDept">
        select d.id, d.dept_id, d.dept_type, d.remark, d.create_user, d.create_time, d.update_user, d.update_time,
               sd.dept_name as deptName
        from production_costing_dept d
        left join sys_dept sd on sd.dept_id = d.dept_id
        order by d.dept_type asc, d.id asc
    </select>
</mapper>
src/main/resources/mapper/production/ProductionCostingFactorMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
<?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.production.mapper.ProductionCostingFactorMapper">
    <select id="selectFactorList" resultType="com.ruoyi.production.pojo.ProductionCostingFactor">
        select f.id, f.process_id, f.staff_id, f.staff_name, f.violin_factor, f.viola_factor, f.bass_factor, f.remark,
               f.create_user, f.create_time, f.update_user, f.update_time,
               too.name as processName
        from production_costing_factor f
        left join technology_operation too on too.id = f.process_id
        <where>
            <if test="c != null">
                <if test="c.processId != null">
                    and f.process_id = #{c.processId}
                </if>
                <if test="c.staffId != null">
                    and f.staff_id = #{c.staffId}
                </if>
                <if test="c.staffName != null and c.staffName != ''">
                    and f.staff_name like concat('%', #{c.staffName}, '%')
                </if>
            </if>
        </where>
        order by f.process_id asc, f.staff_id asc
    </select>
</mapper>
src/main/resources/mapper/production/ProductionCostingMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
<?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.production.mapper.ProductionCostingMapper">
    <select id="selectCostingRows" resultType="com.ruoyi.production.bean.vo.ProductionCostingRawVo">
        select
            ppm.user_id                              as staffId,
            ppm.user_name                            as staffName,
            date_format(ppm.create_time, '%Y-%m')    as yearMonth,
            too.id                                   as processId,
            too.name                                 as processName,
            cd.dept_type                             as deptType,
            ppo.product_model_id                     as productModelId,
            p.product_name                           as productName,
            pm.model                                 as modelName,
            sum(ifnull(ppo.quantity, 0))             as quantity
        from production_product_main ppm
        inner join production_operation_task pot on pot.id = ppm.production_operation_task_id
        inner join production_order_routing_operation poro on poro.id = pot.production_order_routing_operation_id
        inner join technology_operation too on too.id = poro.technology_operation_id
        inner join production_costing_dept cd on cd.dept_id = too.dept_id
        inner join production_product_output ppo on ppo.production_product_main_id = ppm.id
        left join product_model pm on pm.id = ppo.product_model_id
        left join product p on p.id = pm.product_id
        <where>
            ppm.audit_status = 1
            and ppm.report_type in (0, 1)
            <if test="c != null">
                <if test="c.deptType != null">
                    and cd.dept_type = #{c.deptType}
                </if>
                <if test="c.yearMonth != null and c.yearMonth != ''">
                    and date_format(ppm.create_time, '%Y-%m') = #{c.yearMonth}
                </if>
                <if test="c.staffName != null and c.staffName != ''">
                    and ppm.user_name like concat('%', #{c.staffName}, '%')
                </if>
                <if test="c.processId != null">
                    and too.id = #{c.processId}
                </if>
            </if>
        </where>
        group by ppm.user_id, ppm.user_name, yearMonth, too.id, too.name, cd.dept_type, ppo.product_model_id, p.product_name, pm.model
        order by yearMonth desc, ppm.user_id, too.id
    </select>
</mapper>
src/main/resources/mapper/production/ProductionCostingStandardMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
<?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.production.mapper.ProductionCostingStandardMapper">
    <select id="selectStandardList" resultType="com.ruoyi.production.pojo.ProductionCostingStandard">
        select s.id, s.dept_type, s.process_id, s.product_model_id, s.standard_quantity, s.unit, s.remark,
               s.create_user, s.create_time, s.update_user, s.update_time,
               too.name as processName,
               pm.model as modelName
        from production_costing_standard s
        left join technology_operation too on too.id = s.process_id
        left join product_model pm on pm.id = s.product_model_id
        <where>
            <if test="c != null">
                <if test="c.deptType != null">
                    and s.dept_type = #{c.deptType}
                </if>
                <if test="c.processId != null">
                    and s.process_id = #{c.processId}
                </if>
                <if test="c.productModelId != null">
                    and s.product_model_id = #{c.productModelId}
                </if>
            </if>
        </where>
        order by s.dept_type asc, s.process_id asc, s.product_model_id asc
    </select>
</mapper>
src/main/resources/mapper/production/ProductionCostingWorkdayMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
<?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.production.mapper.ProductionCostingWorkdayMapper">
    <select id="selectWorkdayList" resultType="com.ruoyi.production.pojo.ProductionCostingWorkday">
        select w.id, w.work_month as yearMonth, w.work_days, w.remark,
               w.create_user, w.create_time, w.update_user, w.update_time
        from production_costing_workday w
        <where>
            <if test="c != null">
                <if test="c.yearMonth != null and c.yearMonth != ''">
                    and w.work_month = #{c.yearMonth}
                </if>
            </if>
        </where>
        order by w.work_month desc
    </select>
</mapper>