liu
昨天 3e144b2eb8fc9447d2d092edb71db533d12abc52
feat(生产核算): 新增大类「中提琴」并打通工资/核算折算

- 新建 MajorCategoryEnum 作为大类判定唯一事实源(小提琴/中提琴/大提琴/贝斯,
兼容现场把「贝斯」写作「贝司」,排除琴盒/琴套/弓盒等配件),
报工侧与下游共用同一套规则
- 报工大类由三类扩为四类,ProductionProductMainServiceImpl 改为委托枚举
- 工资核算与生产核算改用 production_product_main.major_category 归类,
不再拿产品名称做 equals 硬匹配(产品名带型号后缀时匹配不上,
「小提琴400」「大提琴80L」原本被当成非乐器产品)
- 中提琴折算率与系数另起新列 mid_violin_factor;
历史列 viola_factor 字段名为中提琴、实际语义是大提琴,保持不动并补注释说明
- 生产核算配置表加入数据权限忽略列表,避免注入不存在的 dept_id 列导致 SQL 报错

Co-Authored-By: Claude <noreply@anthropic.com>
已添加1个文件
已修改14个文件
359 ■■■■■ 文件已修改
src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionProductMainDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionCostingDetailVo.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionCostingRawVo.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/enums/MajorCategoryEnum.java 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionCostingFactor.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionCostingServiceImpl.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/pojo/TechnologyOperation.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyOperationServiceImpl.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionAccountMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionCostingFactorMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionCostingMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyOperationMapper.xml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java
@@ -39,6 +39,11 @@
        IGNORE_TABLES.add("product_model");
        IGNORE_TABLES.add("ai_chat_session");
        IGNORE_TABLES.add("product");
        // ç”Ÿäº§æ ¸ç®—配置表没有 dept_id åˆ—,若被追加部门数据权限条件会直接 SQL æŠ¥é”™
        IGNORE_TABLES.add("production_costing_process");
        IGNORE_TABLES.add("production_costing_standard");
        IGNORE_TABLES.add("production_costing_factor");
        IGNORE_TABLES.add("production_costing_workday");
    }
}
src/main/java/com/ruoyi/production/bean/dto/ProductionProductMainDto.java
@@ -72,6 +72,9 @@
    @Schema(description = "产品名称")
    private String productName;
    @Schema(description = "报工产品大类:小提琴/中提琴/大提琴/贝斯,未归类为 null")
    private String majorCategory;
    @Schema(description = "产品规格型号")
    private String productModelName;
src/main/java/com/ruoyi/production/bean/vo/ProductionCostingDetailVo.java
@@ -39,6 +39,8 @@
    @Excel(name = "小提琴数量")
    private BigDecimal violinQty;
    @Excel(name = "中提琴数量")
    private BigDecimal violaQty;
    @Excel(name = "大提琴数量")
    private BigDecimal celloQty;
    @Excel(name = "贝斯数量")
@@ -46,6 +48,9 @@
    @Excel(name = "小提琴折算率")
    private BigDecimal violinFactor;
    @Excel(name = "中提琴折算率")
    private BigDecimal violaFactor;
    /** å¤§æç´æŠ˜ç®—率 */
    @Excel(name = "大提琴折算率")
    private BigDecimal celloFactor;
    @Excel(name = "贝斯折算率")
src/main/java/com/ruoyi/production/bean/vo/ProductionCostingRawVo.java
@@ -22,7 +22,12 @@
    private String processName;
    private Integer deptType;
    private Long productModelId;
    /** äº§å“å¤§ç±»ï¼ˆproduct.product_name),乐器判断依据 */
    /**
     * æŠ¥å·¥äº§å“å¤§ç±»ï¼ˆproduction_product_main.major_category):小提琴/中提琴/大提琴/贝斯。
     * è¿™æ˜¯ä¹å™¨å½’类的判断依据;未归类的报工为 null,由调用方按“非乐器产品”处理。
     */
    private String majorCategory;
    /** äº§å“åç§°ï¼ˆproduct.product_name),仅作展示,不参与归类判断 */
    private String productName;
    /** åž‹å·/规格(product_model.model) */
    private String modelName;
src/main/java/com/ruoyi/production/enums/MajorCategoryEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
package com.ruoyi.production.enums;
import lombok.Getter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
 * ç”Ÿäº§æŠ¥å·¥äº§å“å¤§ç±»ã€‚
 *
 * <p>报工只区分乐器大类(小提琴/中提琴/大提琴/贝斯),不再区分零件。
 * å¤§ç±»åŒæ—¶æ˜¯ã€ŒæŠ˜åˆå°æç´å½“量」折算的匹配键,因此这里的规范值必须与
 * å‰ç«¯ä¸‹æ‹‰é€‰é¡¹ã€{@code production_product_main.major_category} è½åº“值保持一致。</p>
 *
 * <p>本枚举是大类判定的<b>唯一事实源</b>:报工写入时按产品名称归类;
 * å·¥èµ„核算与生产核算直接读取已落库的大类字段,不再各自做字符串比较。</p>
 *
 * @since 2026/09/24
 */
@Getter
public enum MajorCategoryEnum {
    VIOLIN("小提琴", "小提琴"),
    VIOLA("中提琴", "中提琴"),
    CELLO("大提琴", "大提琴"),
    BASS("贝斯", "贝斯", "贝司");
    /**
     * é…ä»¶æŽ’除词:琴盒/琴套/弓盒等配件名称里可能出现「贝司」「大提套」等字样
     * ï¼ˆå¦‚“SA贝司套”“SA大提套”),先排除,避免被误归入乐器大类。
     */
    private static final List<String> ACCESSORY_EXCLUDES = List.of("琴盒", "琴套", "弓盒");
    /**
     * å¤§ç±»è§„范值 -> è¯¥å¤§ç±»çš„产品名称匹配别名。
     * çŽ°åœºæ•°æ®æŠŠã€Œè´æ–¯ã€å†™ä½œã€Œè´å¸ã€ï¼Œæ•… BASS é…äº†ä¸¤ä¸ªåˆ«åã€‚
     */
    private static final Map<String, List<String>> CATEGORY_ALIASES = new LinkedHashMap<>();
    static {
        for (MajorCategoryEnum item : values()) {
            CATEGORY_ALIASES.put(item.category, List.of(item.aliases));
        }
    }
    /** å¤§ç±»è§„范值,落库到 production_product_main.major_category */
    private final String category;
    /** ç”¨äºŽåŒ¹é…äº§å“åç§°çš„别名,第一个与规范值相同 */
    private final String[] aliases;
    MajorCategoryEnum(String category, String... aliases) {
        this.category = category;
        this.aliases = aliases;
    }
    /**
     * æŒ‰äº§å“åç§°å½’入乐器大类。自底向上遍历产品树时逐层调用,命中即返回。
     *
     * @param productName äº§å“åç§°ï¼Œå¯ä¸º null
     * @return å¤§ç±»è§„范值;配件、非乐器产品、空值一律返回 null
     */
    public static String match(String productName) {
        if (productName == null || productName.isEmpty()) {
            return null;
        }
        if (ACCESSORY_EXCLUDES.stream().anyMatch(productName::contains)) {
            return null;
        }
        for (Map.Entry<String, List<String>> entry : CATEGORY_ALIASES.entrySet()) {
            for (String alias : entry.getValue()) {
                if (productName.contains(alias)) {
                    return entry.getKey();
                }
            }
        }
        return null;
    }
    /**
     * æŒ‰å¤§ç±»è§„范值取枚举。
     *
     * @param category å¤§ç±»è§„范值,可为 null
     * @return å¯¹åº”枚举;null æˆ–未知取值返回 null
     */
    public static MajorCategoryEnum of(String category) {
        if (category == null || category.isEmpty()) {
            return null;
        }
        for (MajorCategoryEnum item : values()) {
            if (item.category.equals(category)) {
                return item;
            }
        }
        return null;
    }
    /**
     * åˆ¤æ–­æ˜¯å¦æ˜¯ä¹å™¨å¤§ç±»ã€‚报工未归类(大类为 null)时返回 false,
     * ç”±è°ƒç”¨æ–¹èµ°ã€Œéžä¹å™¨äº§å“ã€åˆ†æ”¯å¤„理。
     */
    public static boolean isInstrument(String category) {
        return of(category) != null;
    }
    /**
     * å…¨éƒ¨å¤§ç±»è§„范值,按枚举声明顺序。
     */
    public static List<String> allCategories() {
        List<String> categories = new ArrayList<>(values().length);
        for (MajorCategoryEnum item : values()) {
            categories.add(item.category);
        }
        return categories;
    }
}
src/main/java/com/ruoyi/production/pojo/ProductionCostingFactor.java
@@ -39,9 +39,13 @@
    @Schema(description = "小提琴折算率(基准)")
    private BigDecimal violinFactor;
    /** å¤§æç´æŠ˜ç®—率。字段名沿用了 viola(中提琴)属于历史错名,语义确为大提琴,勿改 */
    @Schema(description = "大提琴折算率")
    private BigDecimal violaFactor;
    @Schema(description = "中提琴折算率")
    private BigDecimal midViolinFactor;
    @Schema(description = "贝斯折算率")
    private BigDecimal bassFactor;
src/main/java/com/ruoyi/production/service/impl/ProductionCostingServiceImpl.java
@@ -5,6 +5,7 @@
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.enums.MajorCategoryEnum;
import com.ruoyi.production.mapper.ProductionCostingProcessMapper;
import com.ruoyi.production.mapper.ProductionCostingFactorMapper;
import com.ruoyi.production.mapper.ProductionCostingMapper;
@@ -44,9 +45,11 @@
    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 = "贝斯";
    // å¤§ç±»è§„范值统一取自 MajorCategoryEnum,避免规则散落
    private static final String VIOLIN = MajorCategoryEnum.VIOLIN.getCategory();
    private static final String VIOLA = MajorCategoryEnum.VIOLA.getCategory();
    private static final String CELLO = MajorCategoryEnum.CELLO.getCategory();
    private static final String BASS = MajorCategoryEnum.BASS.getCategory();
    private final ProductionCostingMapper productionCostingMapper;
    private final ProductionCostingStandardMapper standardMapper;
@@ -121,19 +124,22 @@
                    vo.setDeptType(row.getDeptType());
                    vo.setQuantity(BigDecimal.ZERO);
                    vo.setViolinQty(BigDecimal.ZERO);
                    vo.setViolaQty(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())) {
                // æŒ‰æŠ¥å·¥æ—¶å½’集好的大类分流;未归类(如零件/原料类报工)与空值一律按小提琴当量 1:1 è®¡å…¥ï¼Œ
                // ä¸Žæ”¹åŠ¨å‰â€œéžä¹å™¨äº§å“â€åˆ†æ”¯çš„è¡Œä¸ºä¿æŒä¸€è‡´ã€‚
                String category = row.getMajorCategory();
                if (VIOLA.equals(category)) {
                    vo.setViolaQty(vo.getViolaQty().add(qty));
                } else if (CELLO.equals(category)) {
                    vo.setCelloQty(vo.getCelloQty().add(qty));
                } else if (BASS.equals(row.getProductName())) {
                } else if (BASS.equals(category)) {
                    vo.setBassQty(vo.getBassQty().add(qty));
                } else {
                    // éžä¹å™¨äº§å“é»˜è®¤æŒ‰å°æç´å½“量 1:1 è®¡å…¥
                    vo.setViolinQty(vo.getViolinQty().add(qty));
                }
            }
@@ -143,13 +149,17 @@
        for (ProductionCostingDetailVo vo : finishedGroup.values()) {
            ProductionCostingFactor factor = factorMap.get(key(vo.getProcessId(), vo.getStaffId()));
            BigDecimal violinFactor = factor == null ? ONE : nzDefault(factor.getViolinFactor(), ONE);
            // æ³¨æ„ï¼šgetViolaFactor() çš„列名是“中提琴”,但历史语义是【大提琴】折算率,勿改
            BigDecimal celloFactor = factor == null ? ONE : nzDefault(factor.getViolaFactor(), ONE);
            BigDecimal violaFactor = factor == null ? ONE : nzDefault(factor.getMidViolinFactor(), ONE);
            BigDecimal bassFactor = factor == null ? ONE : nzDefault(factor.getBassFactor(), ONE);
            vo.setViolinFactor(violinFactor);
            vo.setCelloFactor(celloFactor);
            vo.setViolaFactor(violaFactor);
            vo.setBassFactor(bassFactor);
            BigDecimal folded = nz(vo.getViolinQty()).multiply(violinFactor)
                    .add(nz(vo.getViolaQty()).multiply(violaFactor))
                    .add(nz(vo.getCelloQty()).multiply(celloFactor))
                    .add(nz(vo.getBassQty()).multiply(bassFactor))
                    .setScale(2, RoundingMode.HALF_UP);
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -23,6 +23,7 @@
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.enums.MajorCategoryEnum;
import com.ruoyi.production.enums.ProductOrderStatusEnum;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.pojo.*;
@@ -63,16 +64,6 @@
@AllArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class ProductionProductMainServiceImpl extends ServiceImpl<ProductionProductMainMapper, ProductionProductMain> implements ProductionProductMainService {
    /**
     * ç”Ÿäº§æŠ¥å·¥å¤§ç±»ï¼šåªåŒºåˆ†å°æç´/大提琴/贝斯,不再区分零件。
     * key为入库规范值,value为产品名称匹配别名(现场数据贝斯写作“贝司”)。
     */
    private static final Map<String, List<String>> MAJOR_CATEGORY_PATTERNS = new LinkedHashMap<>() {{
        put("小提琴", List.of("小提琴"));
        put("大提琴", List.of("大提琴"));
        put("贝斯", List.of("贝斯", "贝司"));
    }};
    private final ProductionProductMainMapper productionProductMainMapper;
    private final SysUserMapper userMapper;
@@ -741,7 +732,7 @@
    }
    /**
     * éœ€æ±‚:生产报工只区分产品大类(小提琴/大提琴/贝斯),不再区分零件。
     * éœ€æ±‚:生产报工只区分产品大类(小提琴/中提琴/大提琴/贝斯),不再区分零件。
     * ä»Žäº§å‡ºäº§å“è‡ªèº«åç§°å¼€å§‹æ²¿äº§å“æ ‘向上做关键字匹配,命中即取对应规范大类;未命中返回 null。
     * äº§å“æ ‘顶层为“成品/半成品/原料”等库存分类,不能直接当作大类,因此必须自底向上取第一个命中的层级。
     */
@@ -764,22 +755,12 @@
        return null;
    }
    /**
     * æŒ‰äº§å“åç§°å½’入乐器大类。判定规则(别名表、配件排除)集中在 {@link MajorCategoryEnum},
     * å·¥èµ„核算、生产核算也共用同一套规则,避免各处再写一份字符串比较。
     */
    static String matchMajorCategory(String productName) {
        if (productName == null || productName.isEmpty()) {
            return null;
        }
        // ç´ç›’/琴套等配件名称可能带“贝司”等字样(如 SA贝司套),排除后避免误归大类。
        if (productName.contains("琴盒") || productName.contains("琴套") || productName.contains("弓盒")) {
            return null;
        }
        for (Map.Entry<String, List<String>> entry : MAJOR_CATEGORY_PATTERNS.entrySet()) {
            for (String alias : entry.getValue()) {
                if (productName.contains(alias)) {
                    return entry.getKey();
                }
            }
        }
        return null;
        return MajorCategoryEnum.match(productName);
    }
    private Boolean removeProductMainByProductionTask(ProductionProductMain productionProductMain) {
src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java
@@ -15,6 +15,7 @@
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.bean.dto.ProductionAccountDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.enums.MajorCategoryEnum;
import com.ruoyi.production.pojo.ProductionAccount;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.pojo.TechnologyOperation;
@@ -361,7 +362,7 @@
    /**
     * æ±‡æ€»å‘˜å·¥å½“月月时间合计。
     * è§„则:
     * 1. å°æç´ / å¤§æç´ / è´æ–¯ï¼šæœˆäº§æŠ˜åˆå°æç´æ•°é‡ / å·¥åºæ ‡å‡†æ•°é‡ Ã— 8 + ä¿®è¡¥ç†å·¥æ—¶
     * 1. å°æç´ / ä¸­æç´ / å¤§æç´ / è´æ–¯ï¼šæœˆäº§æŠ˜åˆå°æç´æ•°é‡ / å·¥åºæ ‡å‡†æ•°é‡ Ã— 8 + ä¿®è¡¥ç†å·¥æ—¶
     * 2. å…¶ä»–产品:当月实际产量 / å¯¹åº”产品标准数量 Ã— 8 + ä¿®è¡¥ç†å·¥æ—¶
     * æœ€ç»ˆç»“果就是工资表里的月时间合计。
     */
@@ -380,11 +381,13 @@
            java.math.BigDecimal productStandardQuantity = toDecimal(item.get("productStandardQuantity"));
            java.math.BigDecimal violinFactor = toDecimal(item.get("violinFactor"));
            java.math.BigDecimal violaFactor = toDecimal(item.get("violaFactor"));
            java.math.BigDecimal midViolinFactor = toDecimal(item.get("midViolinFactor"));
            java.math.BigDecimal bassFactor = toDecimal(item.get("bassFactor"));
            String productName = item.get("productName") == null ? null : String.valueOf(item.get("productName"));
            // å½’类依据用报工时已归集好的大类,不再拿产品名称做字符串比较
            String majorCategory = item.get("majorCategory") == null ? null : String.valueOf(item.get("majorCategory"));
            java.math.BigDecimal rowTotal = calculateOperationTime(
                    operationType,
                    productName,
                    majorCategory,
                    finishedNum,
                    workHour,
                    repairWorkHour,
@@ -392,6 +395,7 @@
                    productStandardQuantity,
                    violinFactor,
                    violaFactor,
                    midViolinFactor,
                    bassFactor
            );
            total = total.add(rowTotal).setScale(2, java.math.RoundingMode.HALF_UP);
@@ -402,11 +406,11 @@
    /**
     * è®¡ç®—单条工序的折算工时。
     * è¿™é‡Œæ˜¯æœˆæ—¶é—´åˆè®¡çš„æ ¸å¿ƒåˆ†æ”¯ï¼š
     * - å°æç´ / å¤§æç´ / è´æ–¯ï¼šæœˆäº§æŠ˜åˆå°æç´æ•°é‡ / å·¥åºæ ‡å‡†æ•°é‡ Ã— 8 + ä¿®è¡¥ç†å·¥æ—¶ï¼›
     * - å…¶ä»–产品:当月实际产量 / å¯¹åº”产品标准数量 Ã— 8 + ä¿®è¡¥ç†å·¥æ—¶ã€‚
     * - å°æç´ / ä¸­æç´ / å¤§æç´ / è´æ–¯ï¼šæœˆäº§æŠ˜åˆå°æç´æ•°é‡ / å·¥åºæ ‡å‡†æ•°é‡ Ã— 8 + ä¿®è¡¥ç†å·¥æ—¶ï¼›
     * - å…¶ä»–产品(含未归类报工):当月实际产量 / å¯¹åº”产品标准数量 Ã— 8 + ä¿®è¡¥ç†å·¥æ—¶ã€‚
     */
    private java.math.BigDecimal calculateOperationTime(Integer operationType,
                                                        String productName,
                                                        String majorCategory,
                                                        java.math.BigDecimal finishedNum,
                                                        java.math.BigDecimal workHour,
                                                        java.math.BigDecimal repairWorkHour,
@@ -414,12 +418,12 @@
                                                        java.math.BigDecimal productStandardQuantity,
                                                        java.math.BigDecimal violinFactor,
                                                        java.math.BigDecimal violaFactor,
                                                        java.math.BigDecimal midViolinFactor,
                                                        java.math.BigDecimal bassFactor) {
        if (isInstrumentProduct(productName)) {
            java.math.BigDecimal foldedQuantity = calculateFoldedQuantity(
                    finishedNum,
                    resolveInstrumentFactor(productName, violinFactor, violaFactor, bassFactor)
            );
        java.math.BigDecimal instrumentFactor = resolveInstrumentFactor(
                majorCategory, violinFactor, violaFactor, midViolinFactor, bassFactor);
        if (instrumentFactor != null) {
            java.math.BigDecimal foldedQuantity = calculateFoldedQuantity(finishedNum, instrumentFactor);
            if (standardQuantity.compareTo(java.math.BigDecimal.ZERO) <= 0) {
                return java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP);
            }
@@ -440,9 +444,8 @@
    }
    /**
     * æŒ‰äº§å“åç§°å’Œé…ç½®ç³»æ•°æŠ˜ç®—产量。
     * åªå¯¹å°æç´ã€å¤§æç´ã€è´æ–¯ä¸‰ç±»äº§å“ç”Ÿæ•ˆï¼Œ
     * è¿”回的是可用于后续工序标准数量换算的统一折合产量。
     * æŒ‰äº§é‡ä¸ŽæŠ˜ç®—率折算。是否属于乐器由调用方用 {@link MajorCategoryEnum} åˆ¤å®šï¼Œ
     * è¿™é‡Œåªåšä¹˜æ³•,返回的是可用于后续工序标准数量换算的统一折合产量。
     */
    private java.math.BigDecimal calculateFoldedQuantity(java.math.BigDecimal quantity,
                                                         java.math.BigDecimal factor) {
@@ -452,26 +455,41 @@
        return quantity.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP);
    }
    private boolean isInstrumentProduct(String productName) {
        return "小提琴".equals(productName)
                || "大提琴".equals(productName)
                || "贝斯".equals(productName);
    /**
     * åˆ¤æ–­æŠ¥å·¥å¤§ç±»æ˜¯å¦å±žäºŽä¹å™¨ã€‚未归类({@code major_category} ä¸º null,
     * å¦‚é›¶ä»¶/原料类工序产出)返回 false,由调用方走“其他产品”分支。
     */
    private boolean isInstrumentCategory(String majorCategory) {
        return MajorCategoryEnum.isInstrument(majorCategory);
    }
    private java.math.BigDecimal resolveInstrumentFactor(String productName,
    /**
     * æŒ‰æŠ¥å·¥å¤§ç±»å–对应折算率。
     *
     * @return {@code null} è¡¨ç¤ºä¸æ˜¯ä¹å™¨å¤§ç±»ï¼ˆå«æœªå½’类),调用方据此走“其他产品”分支
     */
    private java.math.BigDecimal resolveInstrumentFactor(String majorCategory,
                                                         java.math.BigDecimal violinFactor,
                                                         java.math.BigDecimal violaFactor,
                                                         java.math.BigDecimal midViolinFactor,
                                                         java.math.BigDecimal bassFactor) {
        if ("小提琴".equals(productName)) {
            return violinFactor;
        MajorCategoryEnum category = MajorCategoryEnum.of(majorCategory);
        if (category == null) {
            return null;
        }
        if ("大提琴".equals(productName)) {
            return violaFactor;
        switch (category) {
            case VIOLIN:
                return violinFactor;
            case VIOLA:
                return midViolinFactor;
            case CELLO:
                // violaFactor çš„列名沿用了 viola(中提琴),历史语义实为【大提琴】折算率,勿改
                return violaFactor;
            case BASS:
                return bassFactor;
            default:
                return null;
        }
        if ("贝斯".equals(productName)) {
            return bassFactor;
        }
        return java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP);
    }
    private String formatEntryDate(java.util.Date entryDate) {
@@ -606,7 +624,7 @@
        Map<String, List<ProductionProductMainDto>> grouped = accountList.stream()
                .filter(item -> item != null && item.getSchedulingUserName() != null)
                .filter(item -> instrumentSheet == isInstrumentProduct(item.getProductName()))
                .filter(item -> instrumentSheet == isInstrumentCategory(item.getMajorCategory()))
                .sorted(java.util.Comparator
                        .comparing(ProductionProductMainDto::getSchedulingUserName, java.util.Comparator.nullsLast(String::compareTo))
                        .thenComparing(item -> StringUtils.isEmpty(item.getProductName()) ? "" : item.getProductName())
@@ -625,7 +643,9 @@
            Map<String, java.math.BigDecimal> convertedMap = new LinkedHashMap<>();
            Map<String, java.math.BigDecimal> foldedMap = new LinkedHashMap<>();
            Map<String, java.math.BigDecimal> violinQuantityMap = new LinkedHashMap<>();
            Map<String, java.math.BigDecimal> violaQuantityMap = new LinkedHashMap<>();
            // å˜é‡åæŒ‰å®žé™…乐器命名:midViolin=中提琴、cello=大提琴(注意勿与历史列名 viola_factor æ··æ·†ï¼‰
            Map<String, java.math.BigDecimal> midViolinQuantityMap = new LinkedHashMap<>();
            Map<String, java.math.BigDecimal> celloQuantityMap = new LinkedHashMap<>();
            Map<String, java.math.BigDecimal> bassQuantityMap = new LinkedHashMap<>();
            for (Map<String, Object> column : columns) {
                String key = String.valueOf(column.get("key"));
@@ -633,7 +653,8 @@
                convertedMap.put(key, java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP));
                foldedMap.put(key, java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP));
                violinQuantityMap.put(key, java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP));
                violaQuantityMap.put(key, java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP));
                midViolinQuantityMap.put(key, java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP));
                celloQuantityMap.put(key, java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP));
                bassQuantityMap.put(key, java.math.BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP));
            }
            for (ProductionProductMainDto item : staffItems) {
@@ -655,11 +676,14 @@
                foldedMap.merge(key, foldedQty, java.math.BigDecimal::add);
                convertedMap.merge(key, convertedHours, java.math.BigDecimal::add);
                if (instrumentSheet) {
                    if ("小提琴".equals(item.getProductName())) {
                    MajorCategoryEnum category = MajorCategoryEnum.of(item.getMajorCategory());
                    if (category == MajorCategoryEnum.VIOLIN) {
                        violinQuantityMap.merge(key, quantity, java.math.BigDecimal::add);
                    } else if ("大提琴".equals(item.getProductName())) {
                        violaQuantityMap.merge(key, quantity, java.math.BigDecimal::add);
                    } else if ("贝斯".equals(item.getProductName())) {
                    } else if (category == MajorCategoryEnum.VIOLA) {
                        midViolinQuantityMap.merge(key, quantity, java.math.BigDecimal::add);
                    } else if (category == MajorCategoryEnum.CELLO) {
                        celloQuantityMap.merge(key, quantity, java.math.BigDecimal::add);
                    } else if (category == MajorCategoryEnum.BASS) {
                        bassQuantityMap.merge(key, quantity, java.math.BigDecimal::add);
                    }
                }
@@ -680,17 +704,29 @@
                createDisplayNumberCell(qtyRow, dataCol, null, numStyle);
                if (instrumentSheet) {
                    Row violaRow = sheet.createRow(rowIdx++);
                    violaRow.setHeightInPoints(24f);
                    createEdgeCell(sheet.getWorkbook(), violaRow, 0, "", numStyle, false, false, true, true);
                    createTextCell(violaRow, 1, "月产大", numStyle);
                    Row midViolinRow = sheet.createRow(rowIdx++);
                    midViolinRow.setHeightInPoints(24f);
                    createEdgeCell(sheet.getWorkbook(), midViolinRow, 0, "", numStyle, false, false, true, true);
                    createTextCell(midViolinRow, 1, "月产中", numStyle);
                    dataCol = 2;
                    for (Map<String, Object> column : columns) {
                        String key = String.valueOf(column.get("key"));
                        createDisplayNumberCell(violaRow, dataCol++, violaQuantityMap.getOrDefault(key, java.math.BigDecimal.ZERO), numStyle);
                        createDisplayNumberCell(midViolinRow, dataCol++, midViolinQuantityMap.getOrDefault(key, java.math.BigDecimal.ZERO), numStyle);
                    }
                    createDisplayNumberCell(violaRow, dataCol++, null, numStyle);
                    createDisplayNumberCell(violaRow, dataCol, null, numStyle);
                    createDisplayNumberCell(midViolinRow, dataCol++, null, numStyle);
                    createDisplayNumberCell(midViolinRow, dataCol, null, numStyle);
                    Row celloRow = sheet.createRow(rowIdx++);
                    celloRow.setHeightInPoints(24f);
                    createEdgeCell(sheet.getWorkbook(), celloRow, 0, "", numStyle, false, false, true, true);
                    createTextCell(celloRow, 1, "月产大", numStyle);
                    dataCol = 2;
                    for (Map<String, Object> column : columns) {
                        String key = String.valueOf(column.get("key"));
                        createDisplayNumberCell(celloRow, dataCol++, celloQuantityMap.getOrDefault(key, java.math.BigDecimal.ZERO), numStyle);
                    }
                    createDisplayNumberCell(celloRow, dataCol++, null, numStyle);
                    createDisplayNumberCell(celloRow, dataCol, null, numStyle);
                    Row bassRow = sheet.createRow(rowIdx++);
                    bassRow.setHeightInPoints(24f);
@@ -795,7 +831,8 @@
                if (product == null || StringUtils.isEmpty(product.getProductName())) {
                    continue;
                }
                if (isInstrumentProduct(product.getProductName())) {
                // ä¹å™¨ç±»äº§å“å½’到“成品部门”sheet,不在这里作为琴盒部等的列
                if (MajorCategoryEnum.match(product.getProductName()) != null) {
                    continue;
                }
                String key = product.getProductName();
@@ -1037,17 +1074,23 @@
            return java.math.BigDecimal.ZERO;
        }
        java.math.BigDecimal quantity = item.getQuantity() == null ? java.math.BigDecimal.ZERO : item.getQuantity();
        String productName = item.getProductName();
        if ("小提琴".equals(productName)) {
            return operation.getViolinFactor() == null ? java.math.BigDecimal.ZERO : quantity.multiply(operation.getViolinFactor());
        MajorCategoryEnum category = MajorCategoryEnum.of(item.getMajorCategory());
        if (category == null) {
            return java.math.BigDecimal.ZERO;
        }
        if ("大提琴".equals(productName)) {
            return operation.getViolaFactor() == null ? java.math.BigDecimal.ZERO : quantity.multiply(operation.getViolaFactor());
        switch (category) {
            case VIOLIN:
                return operation.getViolinFactor() == null ? java.math.BigDecimal.ZERO : quantity.multiply(operation.getViolinFactor());
            case VIOLA:
                return operation.getMidViolinFactor() == null ? java.math.BigDecimal.ZERO : quantity.multiply(operation.getMidViolinFactor());
            case CELLO:
                // violaFactor çš„列名沿用了 viola(中提琴),历史语义实为【大提琴】系数,勿改
                return operation.getViolaFactor() == null ? java.math.BigDecimal.ZERO : quantity.multiply(operation.getViolaFactor());
            case BASS:
                return operation.getBassFactor() == null ? java.math.BigDecimal.ZERO : quantity.multiply(operation.getBassFactor());
            default:
                return java.math.BigDecimal.ZERO;
        }
        if ("贝斯".equals(productName)) {
            return operation.getBassFactor() == null ? java.math.BigDecimal.ZERO : quantity.multiply(operation.getBassFactor());
        }
        return java.math.BigDecimal.ZERO;
    }
    private Map<String, java.math.BigDecimal> buildStandardQuantityMap(List<String> operationNames) {
src/main/java/com/ruoyi/technology/pojo/TechnologyOperation.java
@@ -52,9 +52,13 @@
    @Schema(description = "月产小提琴系数")
    private BigDecimal violinFactor;
    /** æœˆäº§å¤§æç´ç³»æ•°ã€‚字段名沿用了 viola(中提琴)属于历史错名,语义确为大提琴,勿改 */
    @Schema(description = "月产大提琴系数")
    private BigDecimal violaFactor;
    @Schema(description = "月产中提琴系数")
    private BigDecimal midViolinFactor;
    @Schema(description = "月产贝斯系数")
    private BigDecimal bassFactor;
src/main/java/com/ruoyi/technology/service/impl/TechnologyOperationServiceImpl.java
@@ -79,6 +79,7 @@
                        .set(TechnologyOperation::getStandardQuantity, technologyOperation.getStandardQuantity())
                        .set(TechnologyOperation::getViolinFactor, technologyOperation.getViolinFactor())
                        .set(TechnologyOperation::getViolaFactor, technologyOperation.getViolaFactor())
                        .set(TechnologyOperation::getMidViolinFactor, technologyOperation.getMidViolinFactor())
                        .set(TechnologyOperation::getBassFactor, technologyOperation.getBassFactor())
                        .set(TechnologyOperation::getIsQuality, technologyOperation.getIsQuality())
                        .set(TechnologyOperation::getIsProduction, technologyOperation.getIsProduction())
src/main/resources/mapper/production/ProductionAccountMapper.xml
@@ -181,7 +181,9 @@
            ifnull(p.standard_quantity, 0) as productStandardQuantity,
            ifnull(too.violin_factor, 0) as violinFactor,
            ifnull(too.viola_factor, 0) as violaFactor,
            ifnull(too.mid_violin_factor, 0) as midViolinFactor,
            ifnull(too.bass_factor, 0) as bassFactor,
            ppm.major_category as majorCategory,
            p.product_name as productName
        from production_account pa
                 left join production_product_main ppm on ppm.id = pa.production_product_main_id
src/main/resources/mapper/production/ProductionCostingFactorMapper.xml
@@ -3,7 +3,7 @@
<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,
        select f.id, f.process_id, f.staff_id, f.staff_name, f.violin_factor, f.viola_factor, f.mid_violin_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
src/main/resources/mapper/production/ProductionCostingMapper.xml
@@ -11,6 +11,7 @@
            too.name                                 as processName,
            cp.dept_type                             as deptType,
            ppo.product_model_id                     as productModelId,
            ppm.major_category                       as majorCategory,
            p.product_name                           as productName,
            pm.model                                 as modelName,
            sum(ifnull(ppo.quantity, 0))             as quantity
@@ -40,7 +41,7 @@
                </if>
            </if>
        </where>
        group by ppm.user_id, ppm.user_name, yearMonth, too.id, too.name, cp.dept_type, ppo.product_model_id, p.product_name, pm.model
        group by ppm.user_id, ppm.user_name, yearMonth, too.id, too.name, cp.dept_type, ppo.product_model_id, ppm.major_category, p.product_name, pm.model
        order by yearMonth desc, ppm.user_id, too.id
    </select>
src/main/resources/mapper/technology/TechnologyOperationMapper.xml
@@ -14,6 +14,7 @@
        <result column="standard_quantity" property="standardQuantity" />
        <result column="violin_factor" property="violinFactor" />
        <result column="viola_factor" property="violaFactor" />
        <result column="mid_violin_factor" property="midViolinFactor" />
        <result column="bass_factor" property="bassFactor" />
        <result column="conversion_factor" property="conversionFactor" />
        <result column="is_quality" property="isQuality" />