sinake
3 天以前 ee089a8ac4a9d99ef0a0491bbf1d80a2d4fbc393
Merge remote-tracking branch 'origin/master'
已修改17个文件
已添加5个文件
1256 ■■■■■ 文件已修改
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/CasePanoramaController.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/tax/TaxtUtils.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/config/JacksonDateConfig.java 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFund.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetail.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetailExcel.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetailShared.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetailVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFundtax.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/domain/vo/TaxMoneySumEO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/mapper/CasePanoramaMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/mapper/ServiceFundtaxMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/ICasePanoramaService.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/IServiceFunddetailService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFundServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFunddetailServiceImpl.java 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFundtaxServiceImpl.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/resources/mapper/project/CasePanoramaMapper.xml 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/resources/mapper/project/ServiceFundMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/resources/mapper/project/ServiceFunddetailMapper.xml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/resources/mapper/project/ServiceFunddetailSharedMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/resources/mapper/project/ServiceFundtaxMapper.xml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/CasePanoramaController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
package com.ruoyi.web.controller.project;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.project.domain.CasePanorama;
import com.ruoyi.project.domain.CasePanoramaReq;
import com.ruoyi.project.service.ICasePanoramaService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.List;
/**
 * æ¡ˆä¾‹å…¨æ™¯
 *
 * @author ruoyi
 * @date 2026-10-28
 */
@Slf4j
@Api("案例全景")
@RestController
@RequestMapping("/project/casePanorama")
public class CasePanoramaController extends BaseController {
    @Autowired
    private ICasePanoramaService casePanoramaService;
    /**
     * æŸ¥è¯¢æ¡ˆä¾‹å…¨æ™¯
     */
    @ApiOperation("查询案例全景")
    @GetMapping("/list")
    public TableDataInfo list(CasePanoramaReq req) {
        startPage();
        return getDataTable(casePanoramaService.selectCasePanoramaList(req));
    }
    /**
     * å¯¼å‡ºæ¡ˆä¾‹å…¨æ™¯
     */
    @ApiOperation("导出案例全景")
    @Log(title = "案例全景", businessType = BusinessType.EXPORT)
    @GetMapping("/export")
    public AjaxResult export(CasePanoramaReq req) {
        List<CasePanorama> list = casePanoramaService.selectCasePanoramaList(req);
        for (CasePanorama row : list) {
            blankZero(row);
        }
        ExcelUtil<CasePanorama> util = new ExcelUtil<CasePanorama>(CasePanorama.class);
        return util.exportExcel(list, "案例全景数据");
    }
    /**
     * ä¸šåŠ¡ç»„ä¸‹æ‹‰é€‰é¡¹
     */
    @ApiOperation("业务组下拉选项")
    @GetMapping("/deptOptions")
    public AjaxResult deptOptions() {
        return AjaxResult.success(casePanoramaService.selectDeptNames());
    }
    /**
     * é‡‘额 0 å¯¼å‡ºä¸ºç©º
     */
    private void blankZero(CasePanorama row) {
        row.setTotalCost(blankZero(row.getTotalCost()));
        row.setDonorFamilyCost(blankZero(row.getDonorFamilyCost()));
        row.setMedCost(blankZero(row.getMedCost()));
        row.setExpertCost(blankZero(row.getExpertCost()));
        row.setItemCode34(blankZero(row.getItemCode34()));
        row.setItemCode42(blankZero(row.getItemCode42()));
        row.setItemCode46(blankZero(row.getItemCode46()));
        row.setItemCode50(blankZero(row.getItemCode50()));
        row.setItemCode44(blankZero(row.getItemCode44()));
        row.setItemCode47(blankZero(row.getItemCode47()));
        row.setItemCode48(blankZero(row.getItemCode48()));
        row.setItemCode21(blankZero(row.getItemCode21()));
        row.setItemCode12(blankZero(row.getItemCode12()));
        row.setItemCode17(blankZero(row.getItemCode17()));
        row.setItemCode23(blankZero(row.getItemCode23()));
        row.setItemCode25(blankZero(row.getItemCode25()));
    }
    private BigDecimal blankZero(BigDecimal value) {
        return (value == null || value.compareTo(BigDecimal.ZERO) == 0) ? null : value;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/tax/TaxtUtils.java
@@ -66,6 +66,10 @@
    /**
     * é€šè¿‡ç¨ŽåŽå·¥èµ„,算出税前工资
     * <p>
     * æ³¨æ„ï¼šé™¤æ•°å¿…须用字符串/常量构造的精确值。原先写成 {@code new BigDecimal(0.84)} ç­‰ï¼Œ
     * ä¼šæŠŠ double çš„二进制误差带进除法(如 8313.06 / 0.84 å˜æˆ 9896.500000…001),
     * å†å åŠ å‘ä¸Šå–æ•´å°±å¤šå‡º 1 åˆ†é’±ï¼Œå¯¼è‡´â€œç¨ŽåŽåç®—税前”无法还原(8300.00 ç±»é‡‘额反解出 10000.03)。
     *
     * @param money
     * @return
@@ -78,21 +82,21 @@
        }
//第一个参数是除数,第二个参数代表保留几位小数,第三个代表的是使用的模式
        if (money.doubleValue() > 800 && money.doubleValue() <= 3360) {
            taxMoney = money.subtract(new BigDecimal(160)).divide(new BigDecimal(0.8), 2, BigDecimal.ROUND_CEILING);
            taxMoney = money.subtract(new BigDecimal(160)).divide(new BigDecimal("0.8"), 2, BigDecimal.ROUND_CEILING);
        }
        if (money.doubleValue() > 3360 && money.doubleValue() <= 21000) {
            taxMoney = money.divide(new BigDecimal(0.84), 2, BigDecimal.ROUND_CEILING);
            taxMoney = money.divide(new BigDecimal("0.84"), 2, BigDecimal.ROUND_CEILING);
        }
        if (money.doubleValue() > 21000 && money.doubleValue() <= 49500) {
            money = money.subtract(new BigDecimal(2000));
            taxMoney = money.divide(new BigDecimal(0.76), 2, BigDecimal.ROUND_CEILING);
            taxMoney = money.divide(new BigDecimal("0.76"), 2, BigDecimal.ROUND_CEILING);
        }
        if (money.doubleValue() > 49500) {
            money = money.subtract(new BigDecimal(7000));
            taxMoney = money.divide(new BigDecimal(0.68), 2, BigDecimal.ROUND_CEILING);
            taxMoney = money.divide(new BigDecimal("0.68"), 2, BigDecimal.ROUND_CEILING);
        }
ruoyi-framework/src/main/java/com/ruoyi/framework/config/JacksonDateConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,272 @@
package com.ruoyi.framework.config;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
/**
 * æ—¶é—´æ ¼å¼å…¨å±€é…ç½®ï¼šæŽ¥å£å‡ºå‚一律精确到分({@code yyyy-MM-dd HH:mm}),入参宽松兼容带/不带秒。
 *
 * <p><b>为什么用全局配置而不是逐字段改注解:</b>系统里
 * {@code @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")} æœ‰ 182 å¤„、{@code @DateTimeFormat} 73 å¤„,
 * é€ä¸ªæ”¹æ—¢å®¹æ˜“漏(漏一处就是提交 400),也覆盖不到未加注解的 {@code Date} å­—段。
 * è¿™é‡Œç”¨ Jackson çš„ {@link BeanSerializerModifier} / {@link BeanDeserializerModifier}
 * åœ¨<b>不改任何注解</b>的前提下统一格式化口径。
 *
 * <p><b>规则:</b>
 * <ol>
 *   <li>序列化:所有 {@code Date} è¾“出 {@code yyyy-MM-dd HH:mm}(沿用
 *       {@link ApplicationConfig} é…ç½®çš„ JVM é»˜è®¤æ—¶åŒºï¼›å­—段上显式声明了
 *       {@code @JsonFormat(timezone=...)} çš„仍按其时区);</li>
 *   <li>反序列化:兼容 {@code yyyy-MM-dd HH:mm:ss} / {@code yyyy-MM-dd HH:mm} /
 *       {@code yyyy-MM-dd} / æ–œæ åˆ†éš” / ISO8601 / æ—¶é—´æˆ³ â€”— è¿™æ ·å‰ç«¯å›žå¡«"无秒"的值原样提交也不会 400;</li>
 *   <li>运维日志(操作日志/登录日志/定时任务日志)<b>保留秒</b>,便于排查问题。</li>
 * </ol>
 *
 * <p><b>不影响的范围:</b>数据库存储(仍为 datetime,秒位照常保留)、
 * Excel å¯¼å…¥å¯¼å‡ºï¼ˆèµ° {@code @Excel.dateFormat},未改动)、
 * è‡´è¿œååŒæŽ¨é€æŠ¥æ–‡ï¼ˆè‡ªå»º {@code SimpleDateFormat},未改动)、
 * é’‰é’‰æ¶ˆæ¯æ—¶é—´ï¼ˆ{@code FundNotifyMessageBuilder} è‡ªå»ºæ ¼å¼ï¼‰ã€‚
 *
 * @author ruoyi
 */
@Configuration
public class JacksonDateConfig {
    /** æ— ç§’格式 */
    private static final String PATTERN_NO_SECONDS = "yyyy-MM-dd HH:mm";
    /** æœ‰ç§’格式(运维日志用) */
    private static final String PATTERN_WITH_SECONDS = "yyyy-MM-dd HH:mm:ss";
    /** ä¿ç•™ç§’的类:运维日志,秒对排查有用 */
    private static final Set<String> KEEP_SECONDS_CLASSES = new HashSet<String>(Arrays.asList(
            "com.ruoyi.system.domain.SysOperLog",
            "com.ruoyi.system.domain.SysLogininfor",
            "com.ruoyi.quartz.domain.SysJobLog"
    ));
    /** ååºåˆ—化尝试顺序:从最具体到最宽松 */
    private static final String[] PARSE_PATTERNS = new String[]{
            "yyyy-MM-dd HH:mm:ss.SSS",
            "yyyy-MM-dd HH:mm:ss",
            "yyyy-MM-dd HH:mm",
            "yyyy-MM-dd'T'HH:mm:ss.SSS",
            "yyyy-MM-dd'T'HH:mm:ss",
            "yyyy-MM-dd'T'HH:mm",
            "yyyy/MM/dd HH:mm:ss",
            "yyyy/MM/dd HH:mm",
            "yyyy-MM-dd",
            "yyyy/MM/dd",
            "yyyyMMddHHmmss",
            "yyyyMMdd"
    };
    /**
     * 2026-10-15 æ”¹é€ ï¼šä¸å†ç”¨ {@code Jackson2ObjectMapperBuilderCustomizer} + {@code modulesToInstall}。
     *
     * <p><b>为什么改:</b>{@code Jackson2ObjectMapperBuilder.modulesToInstall(...)} çš„语义是
     * "设置"模块列表(不是追加),而 Spring Boot è‡ªèº«ä¹Ÿç”¨åŒä¸€æ–¹æ³•注册它自带的模块(JavaTime ç­‰ï¼‰ï¼›
     * ä¸¤ä¸ªå®šåˆ¶å™¨äº’相覆盖,谁后执行谁的列表生效。实测后果:本模块被丢掉 â€”—
     * è¡¨çŽ°ä¸º"类在包里、bean ä¹Ÿåˆ›å»ºäº†ï¼Œä½† Date çš„宽松解析与去秒序列化都不起作用",
     * å‰ç«¯æŒ‰ {@code yyyy-MM-dd} æäº¤æ—¶ç›´æŽ¥ 400(Cannot deserialize ... expected format yyyy-MM-dd HH:mm:ss)。
     *
     * <p>改为把模块声明成 {@code SimpleModule} bean:Spring Boot é€šè¿‡ {@code ObjectProvider<Module>}
     * ç»Ÿä¸€æ”¶é›†æ‰€æœ‰ Module bean å¹¶ä¸€æ¬¡æ€§å®‰è£…,与定制器的执行顺序无关。
     */
    @Bean
    public SimpleModule dateNoSecondsModule() {
        SimpleModule module = new SimpleModule("dateNoSecondsModule");
        module.addSerializer(Date.class, new NoSecondsDateSerializer());
        module.setSerializerModifier(new NoSecondsDateSerializerModifier());
        module.setDeserializerModifier(new LenientDateDeserializerModifier());
        // å¯åŠ¨è‡ªè¯ï¼šè¿™æ¡æ—¥å¿—å‡ºçŽ°å³è¯´æ˜Žæ¨¡å—å·²è¢«åˆ›å»ºå¹¶ä¼šè¢« Spring Boot å®‰è£…
        LoggerFactory.getLogger(JacksonDateConfig.class).info(
                "[JacksonDateConfig] å·²å¯ç”¨ï¼šDate å‡ºå‚去秒({}),入参宽松兼容({})",
                PATTERN_NO_SECONDS, String.join(" / ", PARSE_PATTERNS));
        return module;
    }
    /**
     * åºåˆ—化器:按分钟输出。
     * åªä½œç”¨äºŽåºåˆ—化,<b>不影响入参解析</b>。
     */
    static class NoSecondsDateSerializer extends JsonSerializer<Date> implements ContextualSerializer {
        private final DateTimeFormatter formatter;
        NoSecondsDateSerializer() {
            this.formatter = null;
        }
        private NoSecondsDateSerializer(TimeZone timeZone, boolean keepSeconds) {
            this.formatter = DateTimeFormatter
                    .ofPattern(keepSeconds ? PATTERN_WITH_SECONDS : PATTERN_NO_SECONDS)
                    .withZone((timeZone == null ? TimeZone.getDefault() : timeZone).toZoneId());
        }
        @Override
        public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
            TimeZone timeZone = resolveTimeZone(prov.getConfig().getTimeZone(),
                    property == null ? null : property.getMember());
            return new NoSecondsDateSerializer(timeZone, isKeepSeconds(property == null ? null : property.getMember()));
        }
        @Override
        public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            if (value == null) {
                gen.writeNull();
                return;
            }
            if (formatter != null) {
                gen.writeString(formatter.format(value.toInstant()));
                return;
            }
            // å…œåº•:未被 contextualize æ—¶æŒ‰å½“前配置时区、无秒输出
            gen.writeString(DateTimeFormatter.ofPattern(PATTERN_NO_SECONDS)
                    .withZone(serializers.getConfig().getTimeZone().toZoneId())
                    .format(value.toInstant()));
        }
    }
    /**
     * è®© Bean å±žæ€§ä¸Šçš„æ—¥æœŸä¹Ÿèµ°ä¸Šé¢çš„序列化器。
     * å­—段级 {@code @JsonFormat} ç”Ÿæˆçš„序列化器优先级更高,必须在这里替换掉,否则注解字段仍带秒。
     */
    static class NoSecondsDateSerializerModifier extends BeanSerializerModifier {
        @Override
        @SuppressWarnings({"unchecked", "rawtypes"})
        public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
                                                         List<BeanPropertyWriter> beanProperties) {
            boolean keepSeconds = KEEP_SECONDS_CLASSES.contains(beanDesc.getBeanClass().getName());
            for (BeanPropertyWriter writer : beanProperties) {
                if (Date.class.isAssignableFrom(writer.getType().getRawClass())) {
                    TimeZone timeZone = resolveTimeZone(config.getTimeZone(), writer.getMember());
                    writer.assignSerializer((JsonSerializer) new NoSecondsDateSerializer(timeZone, keepSeconds));
                }
            }
            return beanProperties;
        }
    }
    /**
     * å…¥å‚解析:兼容带秒/不带秒/纯日期/ISO/时间戳,避免"出参无秒、回填后再提交"导致 400。
     */
    static class LenientDateDeserializerModifier extends BeanDeserializerModifier {
        @Override
        @SuppressWarnings({"unchecked", "rawtypes"})
        public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc,
                                                     BeanDeserializerBuilder builder) {
            Iterator<SettableBeanProperty> properties = builder.getProperties();
            List<SettableBeanProperty> replaced = new ArrayList<SettableBeanProperty>();
            while (properties.hasNext()) {
                SettableBeanProperty property = properties.next();
                if (property.getType() != null && Date.class.isAssignableFrom(property.getType().getRawClass())) {
                    TimeZone timeZone = resolveTimeZone(config.getTimeZone(), property.getMember());
                    replaced.add(property.withValueDeserializer(new LenientDateDeserializer(timeZone)));
                }
            }
            // ä¸èƒ½è¾¹éåŽ†è¾¹æ”¹ï¼Œå…ˆæ”¶é›†å†æ›¿æ¢
            for (SettableBeanProperty property : replaced) {
                builder.addOrReplaceProperty(property, true);
            }
            return builder;
        }
    }
    /** å®½æ¾æ—¥æœŸè§£æžå™¨ */
    static class LenientDateDeserializer extends JsonDeserializer<Date> {
        private final TimeZone timeZone;
        LenientDateDeserializer(TimeZone timeZone) {
            this.timeZone = timeZone == null ? TimeZone.getDefault() : timeZone;
        }
        @Override
        public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
                return new Date(p.getLongValue());
            }
            String text = p.getValueAsString();
            if (text == null || text.trim().isEmpty()) {
                return null;
            }
            text = text.trim();
            for (String pattern : PARSE_PATTERNS) {
                SimpleDateFormat format = new SimpleDateFormat(pattern);
                format.setLenient(false);
                format.setTimeZone(timeZone);
                ParsePosition position = new ParsePosition(0);
                Date parsed = format.parse(text, position);
                // å¿…须整串被消费,否则 "yyyy-MM-dd" ä¼šæŠŠ "2026-10-08 15:23" çš„æ—¶åˆ†é™é»˜ä¸¢æŽ‰
                if (parsed != null && position.getIndex() == text.length()) {
                    return parsed;
                }
            }
            try {
                return Date.from(Instant.parse(text));
            } catch (Exception ignore) {
                // è½åˆ°ä¸‹é¢ç»Ÿä¸€æŠ¥é”™
            }
            throw ctxt.weirdStringException(text, Date.class,
                    "无法解析的时间格式,支持 yyyy-MM-dd[ HH:mm[:ss]]");
        }
    }
    /** å±žæ€§ä¸Šæ˜¾å¼å£°æ˜Žçš„æ—¶åŒºä¼˜å…ˆï¼Œå¦åˆ™ç”¨å…¨å±€é…ç½®ï¼ˆå³ JVM é»˜è®¤æ—¶åŒºï¼‰ */
    private static TimeZone resolveTimeZone(TimeZone fallback, AnnotatedMember member) {
        if (member != null) {
            try {
                JsonFormat jsonFormat = member.getAnnotation(JsonFormat.class);
                if (jsonFormat != null && jsonFormat.timezone() != null
                        && !jsonFormat.timezone().isEmpty()
                        && !"##default".equals(jsonFormat.timezone())) {
                    return TimeZone.getTimeZone(jsonFormat.timezone());
                }
            } catch (Exception ignore) {
                // å–不到注记时用全局时区
            }
        }
        return fallback == null ? TimeZone.getDefault() : fallback;
    }
    /** è¿ç»´æ—¥å¿—类保留秒 */
    private static boolean isKeepSeconds(AnnotatedMember member) {
        return member != null && KEEP_SECONDS_CLASSES.contains(member.getDeclaringClass().getName());
    }
}
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFund.java
@@ -299,10 +299,37 @@
    private Double pretaxcost;
    /**
     * ç¨ŽåŽé‡‘额
     * å¢žå€¼ç¨Žåˆè®¡
     */
    @ApiModelProperty("税后金额")
    @Excel(name = "税后金额")
    @ApiModelProperty("增值税合计")
    @Excel(name = "增值税")
    private Double vatCost;
    /**
     * åŸŽå»ºç¨Žåˆè®¡
     */
    @ApiModelProperty("城建税合计")
    @Excel(name = "城建税")
    private Double cityTaxCost;
    /**
     * åŠ³åŠ¡ç¨Žå‰åˆè®¡
     */
    @ApiModelProperty("劳务税前合计")
    @Excel(name = "劳务税前")
    private Double laborPretaxCost;
    /**
     * ç¨Žå£å¾„版本:v1=历史口径(无增值税/城建税),v2=新增口径
     */
    @ApiModelProperty("税口径版本")
    private String taxVersion;
    /**
     * åŠ³åŠ¡ç¨ŽåŽï¼ˆåŽŸâ€œç¨ŽåŽé‡‘é¢â€ï¼‰
     */
    @ApiModelProperty("劳务税后")
    @Excel(name = "劳务税后")
    private Double taxedcost;
    /**
@@ -387,10 +414,10 @@
    private Long fundTaxId = null;
    /**
     * ç¨Žé‡‘
     * åŠ³åŠ¡ä¸ªç¨Žåˆè®¡ï¼ˆåŽŸâ€œç¨Žé‡‘â€ï¼‰
     */
    @ApiModelProperty("税金")
    @Excel(name = "税金")
    @ApiModelProperty("劳务个税合计")
    @Excel(name = "劳务个税")
    private Double taxcost;
    @ApiModelProperty("协友要求再次发起时,带上这个值")
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetail.java
@@ -217,11 +217,38 @@
    private Double prepaidamount;
    /**
     * æ‰£ç¨Žé‡‘额
     * åŠ³åŠ¡ä¸ªç¨Žï¼ˆåŽŸâ€œæ‰£ç¨Žé‡‘é¢â€ï¼‰
     */
    @ApiModelProperty("扣税金额")
    @Excel(name = "扣税金额")
    @ApiModelProperty("劳务个税")
    @Excel(name = "劳务个税")
    private Double taxamount;
    /**
     * å¢žå€¼ç¨Ž
     */
    @ApiModelProperty("增值税")
    @Excel(name = "增值税")
    private Double vatAmount;
    /**
     * åŸŽå»ºç¨Ž
     */
    @ApiModelProperty("城建税")
    @Excel(name = "城建税")
    private Double cityTaxAmount;
    /**
     * åŠ³åŠ¡ç¨Žå‰é‡‘é¢
     */
    @ApiModelProperty("劳务税前金额")
    @Excel(name = "劳务税前")
    private Double laborPretaxAmount;
    /**
     * ç¨Žå£å¾„版本:v1=历史口径(无增值税/城建税),v2=新增口径
     */
    @ApiModelProperty("税口径版本")
    private String taxVersion;
    /**
     * å‘票张数
@@ -237,10 +264,10 @@
    @Excel(name = "上传时间")
    private BigDecimal reimbursementamount;
    /**
     * ç¨ŽåŽé‡‘额
     * åŠ³åŠ¡ç¨ŽåŽï¼ˆåŽŸâ€œç¨ŽåŽé‡‘é¢â€ï¼‰
     */
    @ApiModelProperty("税后金额")
    @Excel(name = "税后金额")
    @ApiModelProperty("劳务税后")
    @Excel(name = "劳务税后")
    private Double taxedamount;
    /**
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetailExcel.java
@@ -72,13 +72,39 @@
    @Excel(name = "税前金额")
    private Double amount;
    /**
     * å¢žå€¼ç¨Ž
     */
    @ApiModelProperty("增值税")
    @Excel(name = "增值税")
    private Double vatAmount;
    /**
     * ç¨ŽåŽé‡‘额
     * åŸŽå»ºç¨Ž
     */
    @ApiModelProperty("税后金额")
    @Excel(name = "税后金额")
    @ApiModelProperty("城建税")
    @Excel(name = "城建税")
    private Double cityTaxAmount;
    /**
     * åŠ³åŠ¡ç¨Žå‰
     */
    @ApiModelProperty("劳务税前")
    @Excel(name = "劳务税前")
    private Double laborPretaxAmount;
    /**
     * åŠ³åŠ¡ä¸ªç¨Ž
     */
    @ApiModelProperty("劳务个税")
    @Excel(name = "劳务个税")
    private Double taxamount;
    /**
     * åŠ³åŠ¡ç¨ŽåŽ
     */
    @ApiModelProperty("劳务税后")
    @Excel(name = "劳务税后")
    private Double taxedamount;
    /**
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetailShared.java
@@ -242,20 +242,41 @@
    private Double amount;
    /**
     * æ‰£ç¨Žé‡‘额
     * åŠ³åŠ¡ä¸ªç¨Žï¼ˆåŽŸâ€œæ‰£ç¨Žé‡‘é¢â€ï¼‰
     */
    @ApiModelProperty("扣税金额")
    @Excel(name = "扣税金额")
    @ApiModelProperty("劳务个税")
    @Excel(name = "劳务个税")
    private Double taxamount;
    /**
     * ç¨ŽåŽé‡‘额
     * åŠ³åŠ¡ç¨ŽåŽï¼ˆåŽŸâ€œç¨ŽåŽé‡‘é¢â€ï¼‰
     */
    @ApiModelProperty("税后金额")
    @Excel(name = "税后金额")
    @ApiModelProperty("劳务税后")
    @Excel(name = "劳务税后")
    private Double taxedamount;
    /**
     * å¢žå€¼ç¨Ž
     */
    @ApiModelProperty("增值税")
    @Excel(name = "增值税")
    private Double vatAmount;
    /**
     * åŸŽå»ºç¨Ž
     */
    @ApiModelProperty("城建税")
    @Excel(name = "城建税")
    private Double cityTaxAmount;
    /**
     * åŠ³åŠ¡ç¨Žå‰é‡‘é¢
     */
    @ApiModelProperty("劳务税前金额")
    @Excel(name = "劳务税前")
    private Double laborPretaxAmount;
    /**
     * é¢„支金额
     */
    @ApiModelProperty("预支金额")
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetailVO.java
@@ -202,11 +202,38 @@
    private Double prepaidamount;
    /**
     * æ‰£ç¨Žé‡‘额
     * åŠ³åŠ¡ä¸ªç¨Žï¼ˆåŽŸâ€œæ‰£ç¨Žé‡‘é¢â€ï¼‰
     */
    @ApiModelProperty("扣税金额")
    @Excel(name = "扣税金额")
    @ApiModelProperty("劳务个税")
    @Excel(name = "劳务个税")
    private Double taxamount;
    /**
     * å¢žå€¼ç¨Ž
     */
    @ApiModelProperty("增值税")
    @Excel(name = "增值税")
    private Double vatAmount;
    /**
     * åŸŽå»ºç¨Ž
     */
    @ApiModelProperty("城建税")
    @Excel(name = "城建税")
    private Double cityTaxAmount;
    /**
     * åŠ³åŠ¡ç¨Žå‰é‡‘é¢
     */
    @ApiModelProperty("劳务税前金额")
    @Excel(name = "劳务税前")
    private Double laborPretaxAmount;
    /**
     * ç¨Žå£å¾„版本:v1=历史口径(无增值税/城建税),v2=新增口径
     */
    @ApiModelProperty("税口径版本")
    private String taxVersion;
    /**
     * å‘票张数
@@ -216,10 +243,10 @@
    private Long invoicecount;
    /**
     * ç¨ŽåŽé‡‘额
     * åŠ³åŠ¡ç¨ŽåŽï¼ˆåŽŸâ€œç¨ŽåŽé‡‘é¢â€ï¼‰
     */
    @ApiModelProperty("税后金额")
    @Excel(name = "税后金额")
    @ApiModelProperty("劳务税后")
    @Excel(name = "劳务税后")
    private Double taxedamount;
    /**
ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFundtax.java
@@ -98,24 +98,51 @@
    private BigDecimal pretaxamount;
    /**
     * ç¨ŽåŽé‡‘额合计
     * å¢žå€¼ç¨Žåˆè®¡
     */
    @ApiModelProperty("税后金额合计")
    @Excel(name = "税后金额合计")
    @ApiModelProperty("增值税合计")
    @Excel(name = "增值税合计")
    private BigDecimal vatAmount;
    /**
     * åŸŽå»ºç¨Žåˆè®¡
     */
    @ApiModelProperty("城建税合计")
    @Excel(name = "城建税合计")
    private BigDecimal cityTaxAmount;
    /**
     * åŠ³åŠ¡ç¨Žå‰åˆè®¡
     */
    @ApiModelProperty("劳务税前合计")
    @Excel(name = "劳务税前合计")
    private BigDecimal laborPretaxAmount;
    /**
     * ç¨Žå£å¾„版本:v1=历史口径(无增值税/城建税),v2=新增口径
     */
    @ApiModelProperty("税口径版本")
    private String taxVersion;
    /**
     * åŠ³åŠ¡ç¨ŽåŽï¼ˆåŽŸâ€œç¨ŽåŽé‡‘é¢åˆè®¡â€ï¼‰
     */
    @ApiModelProperty("劳务税后合计")
    @Excel(name = "劳务税后合计")
    private BigDecimal taxedamount;
    /**
     * è´¹ç”¨æ€»é‡‘额
     * åŠ³åŠ¡ä¸ªç¨Žåˆè®¡
     */
    @ApiModelProperty("费用总金额")
    @Excel(name = "费用总金额")
    @ApiModelProperty("劳务个税合计")
    @Excel(name = "劳务个税合计")
    private BigDecimal taxamount;
    /**
     * æ‰£ç¨Žé‡‘额
     * åŠ³åŠ¡ä¸ªç¨Žåˆè®¡ï¼ˆåŽ†å²å­—æ®µ tax_money,页面“扣税金额”取此列)
     */
    @ApiModelProperty("扣税金额")
    @Excel(name = "扣税金额")
    @ApiModelProperty("劳务个税(扣税金额)")
    @Excel(name = "劳务个税")
    private BigDecimal taxMoney;
    /**
ruoyi-project/src/main/java/com/ruoyi/project/domain/vo/TaxMoneySumEO.java
@@ -7,19 +7,35 @@
public class TaxMoneySumEO extends BaseEntity {
    /**
     * ç¨Žå‰æ€»é¢
     * ç¨Žå‰æ€»é¢ï¼ˆAmount åˆè®¡ï¼‰
     */
    private double amounts;
    /**
     * ç¨Žæ€»é¢
     * å¢žå€¼ç¨Žæ€»é¢
     */
    private double vatAmounts;
    /**
     * åŸŽå»ºç¨Žæ€»é¢
     */
    private double cityTaxAmounts;
    /**
     * åŠ³åŠ¡ç¨Žå‰æ€»é¢ï¼ˆåŠ³åŠ¡ä¸ªç¨Žç´¯è®¡åŸºæ•°ï¼‰
     */
    private double laborPretaxAmounts;
    /**
     * åŠ³åŠ¡ä¸ªç¨Žæ€»é¢
     */
    private double taxAmounts;
    /**
     * ç¨ŽåŽæ€»é¢
     * åŠ³åŠ¡ç¨ŽåŽæ€»é¢
     */
    private double taxedAmounts;
    /**
     * åºå·
     */
@@ -29,5 +45,9 @@
     */
    private String IDCardNo;
    /**
     * å½“月已存在的历史口径(v1)明细条数,用于阻止同一月份 v1/v2 æ··ç®—
     */
    private Long v1Count;
}
ruoyi-project/src/main/java/com/ruoyi/project/mapper/CasePanoramaMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.project.mapper;
import com.ruoyi.project.domain.CasePanorama;
import com.ruoyi.project.domain.CasePanoramaReq;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * æ¡ˆä¾‹å…¨æ™¯Mapper
 *
 * @author ruoyi
 * @date 2026-10-28
 */
@Mapper
public interface CasePanoramaMapper {
    /**
     * æŸ¥è¯¢æ¡ˆä¾‹å…¨æ™¯
     */
    List<CasePanorama> selectCasePanoramaList(@Param("req") CasePanoramaReq req);
    /**
     * æŸ¥è¯¢ä¸šåŠ¡ç»„é€‰é¡¹
     */
    List<String> selectDeptNames();
}
ruoyi-project/src/main/java/com/ruoyi/project/mapper/ServiceFundtaxMapper.java
@@ -29,4 +29,13 @@
     * @return
     */
    public Integer getMaxFundTaxId();
    /**
     * å¯¹å½“前批次汇总单加行锁(SELECT ... FOR UPDATE),防止并发重复算税。
     * å¿…须在事务中调用。
     *
     * @param id æ±‡æ€»å• ID
     * @return æ±‡æ€»å•(不存在返回 null)
     */
    public ServiceFundtax selectByIdForUpdate(Long id);
}
ruoyi-project/src/main/java/com/ruoyi/project/service/ICasePanoramaService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.ruoyi.project.service;
import com.ruoyi.project.domain.CasePanorama;
import com.ruoyi.project.domain.CasePanoramaReq;
import java.util.List;
/**
 * æ¡ˆä¾‹å…¨æ™¯Service
 *
 * @author ruoyi
 * @date 2026-10-28
 */
public interface ICasePanoramaService {
    /**
     * æŸ¥è¯¢æ¡ˆä¾‹å…¨æ™¯
     */
    List<CasePanorama> selectCasePanoramaList(CasePanoramaReq req);
    /**
     * æŸ¥è¯¢ä¸šåŠ¡ç»„é€‰é¡¹
     */
    List<String> selectDeptNames();
}
ruoyi-project/src/main/java/com/ruoyi/project/service/IServiceFunddetailService.java
@@ -1,5 +1,6 @@
package com.ruoyi.project.service;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -62,12 +63,21 @@
    List<ServiceFunddetailVO> getFundDetail(Long id);
    /**
     * æ ¹æ®æ¡ä»¶ç®—税
     * æ ¹æ®æ¡ä»¶ç®—税(劳务个税 + å¢žå€¼ç¨Ž + åŸŽå»ºç¨Žï¼‰
     *
     * @param serviceFunddetails
     */
    public Boolean calculateTax(List<ServiceFunddetail> serviceFunddetails, Map<String, TaxMoneySumEO> tempTaxSumMap);
    /**
     * æ ¹æ®æ¡ä»¶ç®—税,并指定本批算税时间(同批明细 tax_time ä¸€è‡´ï¼‰
     *
     * @param serviceFunddetails æ˜Žç»†åˆ—表
     * @param tempTaxSumMap      æœ¬æœˆç´¯è®¡ç¼“å­˜
     * @param taxTime            æœ¬æ¬¡ç®—税时间
     */
    public Boolean calculateTax(List<ServiceFunddetail> serviceFunddetails, Map<String, TaxMoneySumEO> tempTaxSumMap, Date taxTime);
//    public Boolean calculateTax2(List<ServiceFunddetail> serviceFunddetails);
    /**
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFundServiceImpl.java
@@ -1705,10 +1705,23 @@
            //通过受益人名称,将税前、税后的值相加
            Map<String, Double> sumAmount = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname, Collectors.summingDouble(ServiceFunddetail::getAmount)));
            Map<String, Double> sumTaxdAmount = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname, Collectors.summingDouble(ServiceFunddetail::getTaxedamount)));
            // æ–°å¢žå£å¾„:增值税、城建税、劳务税前、劳务个税按受益人合计
            Map<String, Double> sumVat = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname,
                    Collectors.summingDouble(d -> d.getVatAmount() == null ? 0.0 : d.getVatAmount())));
            Map<String, Double> sumCityTax = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname,
                    Collectors.summingDouble(d -> d.getCityTaxAmount() == null ? 0.0 : d.getCityTaxAmount())));
            Map<String, Double> sumLaborPretax = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname,
                    Collectors.summingDouble(d -> d.getLaborPretaxAmount() == null ? 0.0 : d.getLaborPretaxAmount())));
            Map<String, Double> sumTax = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname,
                    Collectors.summingDouble(d -> d.getTaxamount() == null ? 0.0 : d.getTaxamount())));
            for (String key : sumAmount.keySet()) {
                ServiceFunddetailExcel serviceFunddetailExcel = new ServiceFunddetailExcel();
                serviceFunddetailExcel.setAmount(sumAmount.getOrDefault(key, 0.0));
                serviceFunddetailExcel.setVatAmount(sumVat.getOrDefault(key, 0.0));
                serviceFunddetailExcel.setCityTaxAmount(sumCityTax.getOrDefault(key, 0.0));
                serviceFunddetailExcel.setLaborPretaxAmount(sumLaborPretax.getOrDefault(key, 0.0));
                serviceFunddetailExcel.setTaxamount(sumTax.getOrDefault(key, 0.0));
                serviceFunddetailExcel.setTaxedamount(sumTaxdAmount.getOrDefault(key, 0.0));
                for (ServiceFunddetail serviceFunddetail2 : serviceFunddetails) {
                    if (serviceFunddetail2.getBeneficiaryname().equals(key)) {
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFunddetailServiceImpl.java
@@ -13,6 +13,7 @@
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.tax.PerformanceTaxtUtils;
import com.ruoyi.common.tax.TaxtUtils;
import com.ruoyi.common.tax.VatTaxUtils;
import com.ruoyi.common.utils.bean.DtoConversionUtils;
import com.ruoyi.project.domain.*;
import com.ruoyi.project.domain.vo.*;
@@ -253,15 +254,22 @@
        Long fundid = serviceFunddetail.getFundid();
        ServiceFund serviceFund = serviceFundMapper.selectById(fundid);
        if (ObjectUtils.isNotEmpty(serviceFund)) {
            Double pretax = serviceFund.getPretaxcost();
            Double aftertax = serviceFund.getTaxedcost();
            pretax = pretax - serviceFunddetail.getAmount();
            aftertax = aftertax - serviceFunddetail.getTaxedamount();
            Double amount = pretax + aftertax;
            // é‡‘额一律用 BigDecimal ç´¯åŠ /扣减,避免浮点尾差
            BigDecimal pretax = toMoney(serviceFund.getPretaxcost()).subtract(toMoney(serviceFunddetail.getAmount()));
            BigDecimal aftertax = toMoney(serviceFund.getTaxedcost()).subtract(toMoney(serviceFunddetail.getTaxedamount()));
            BigDecimal vat = toMoney(serviceFund.getVatCost()).subtract(toMoney(serviceFunddetail.getVatAmount()));
            BigDecimal cityTax = toMoney(serviceFund.getCityTaxCost()).subtract(toMoney(serviceFunddetail.getCityTaxAmount()));
            BigDecimal laborPretax = toMoney(serviceFund.getLaborPretaxCost()).subtract(toMoney(serviceFunddetail.getLaborPretaxAmount()));
            BigDecimal tax = toMoney(serviceFund.getTaxcost()).subtract(toMoney(serviceFunddetail.getTaxamount()));
            BigDecimal amount = pretax.add(aftertax);
            serviceFund.setPretaxcost(pretax);
            serviceFund.setTaxedcost(aftertax);
            serviceFund.setAmountrequested(amount);
            serviceFund.setPretaxcost(pretax.doubleValue());
            serviceFund.setTaxedcost(aftertax.doubleValue());
            serviceFund.setVatCost(vat.doubleValue());
            serviceFund.setCityTaxCost(cityTax.doubleValue());
            serviceFund.setLaborPretaxCost(laborPretax.doubleValue());
            serviceFund.setTaxcost(tax.doubleValue());
            serviceFund.setAmountrequested(amount.doubleValue());
            int updateAmount = serviceFundMapper.updateById(serviceFund);
        }
        return serviceFunddetailMapper.deleteById(id);
@@ -285,145 +293,228 @@
    }
    /**
     * æ‰¹é‡ç®—税主方法
     * 1. æ ¡éªŒå‚æ•°
     * 2. æŽ’序明细,保证税时间为空的排在后面
     * 3. éåŽ†æ¯æ¡æ˜Žç»†ï¼Œåˆ†èº«ä»½è¯å·ä¸ºç©ºå’Œä¸ä¸ºç©ºä¸¤ç§æƒ…å†µåˆ†åˆ«å¤„ç†
     * - èº«ä»½è¯å·ä¸ºç©ºï¼šç›´æŽ¥æŒ‰è§„则赋值税后金额、税金
     * - èº«ä»½è¯å·ä¸ä¸ºç©ºï¼š
     * a. æŸ¥è¯¢æœ¬æœˆç¬¬ä¸€å¤©ï¼ŒèŽ·å–æœ€å¤§åºå·
     * b. æŸ¥è¯¢æœ¬æœˆç´¯è®¡ç¨Žå‰ã€ç¨Žé‡‘、税后金额
     * c. æ ¹æ®"税前/税后"类型分别计算税金、税后金额
     * d. è®¾ç½®ç®—税时间、序号等
     * 4. ç´¯è®¡æœ¬æ‰¹æ¬¡æ€»ç¨Žå‰ã€ç¨ŽåŽé‡‘额
     * 5. æ‰¹é‡æ›´æ–°æ‰€æœ‰æ˜Žç»†
     * 6. æ›´æ–°ä¸»è¡¨ServiceFund的总金额、算税时间、状态
     * æ‰¹é‡ç®—税主方法(劳务个税 + å¢žå€¼ç¨Ž + åŸŽå»ºç¨Žï¼Œv2 å£å¾„)
     * <p>
     * é‡‘额模型:
     * <pre>
     * ç¨Žå‰å½•入:V = ceil(P Ã— 1%)、C = ceil(V Ã— 3.5%)、L = P - V - C、
     *          T = åŠ³åŠ¡ä¸ªç¨Ž(当月累计 L) - åŽŸç´¯è®¡ T、N = L - T
     * ç¨ŽåŽå½•入:N累计 = åŽŸç´¯è®¡ N + N、L累计 = getTaxationBefore(N累计)、
     *          L = L累计 - åŽŸç´¯è®¡ L、T = L - N、P/V/C = solveGross(L)
     * æ— èº«ä»½è¯ï¼šV = C = T = 0,L = N = P
     * </pre>
     *
     * @param serviceFunddetails è´¹ç”¨æ˜Žç»†åˆ—表
     * @param tempTaxSumMap      æœ¬æœˆç´¯è®¡é‡‘额缓存(按身份证号)
     * @return æ˜¯å¦ç®—税成功
     */
    @Override
    public Boolean calculateTax(List<ServiceFunddetail> serviceFunddetails, Map<String, TaxMoneySumEO> tempTaxSumMap) {
        return calculateTax(serviceFunddetails, tempTaxSumMap, new Date());
    }
    /**
     * æ‰¹é‡ç®—税(指定本批算税时间,保证同批明细 tax_time ä¸€è‡´ï¼‰
     *
     * @param serviceFunddetails è´¹ç”¨æ˜Žç»†åˆ—表
     * @param tempTaxSumMap      æœ¬æœˆç´¯è®¡é‡‘额缓存(按身份证号)
     * @param taxTime            æœ¬æ¬¡ç®—税时间
     * @return æ˜¯å¦ç®—税成功
     */
    @Override
    public Boolean calculateTax(List<ServiceFunddetail> serviceFunddetails, Map<String, TaxMoneySumEO> tempTaxSumMap, Date taxTime) {
        // 1. æ ¡éªŒå‚æ•°
        if (CollectionUtils.isEmpty(serviceFunddetails)) {
            throw new BaseException("算税条件为空,请检查后,再进行计算");
        }
        // 2. æŽ’序,税时间为空的排后面
        serviceFunddetails.sort(Comparator.comparing(obj -> ObjectUtils.isEmpty(obj.getTaxTime()) ? 1 : 0));
        // 2. æŽ’序:税时间为空的排后面,再按 id ä¿è¯åŒæ‰¹é¡ºåºç¨³å®š
        serviceFunddetails.sort(Comparator
                .comparing((ServiceFunddetail obj) -> ObjectUtils.isEmpty(obj.getTaxTime()) ? 1 : 0)
                .thenComparing(obj -> obj.getId() == null ? Long.MAX_VALUE : obj.getId()));
        // 3. èŽ·å–æœ¬æ‰¹æ¬¡fundid及主表信息
        // 3. èŽ·å–æœ¬æ‰¹æ¬¡ fundid åŠä¸»è¡¨ä¿¡æ¯
        Long fundid = serviceFunddetails.get(0).getFundid();
        log.info("算税的fundid的值为:{}", fundid);
        ServiceFund serviceFund1 = serviceFundMapper.selectById(fundid);
        log.info("fund表的fundtextime值为:{}", serviceFund1.getFundtaxtime());
        // 4. åˆå§‹åŒ–累计金额、待更新明细列表、序号缓存、累计金额缓存
        BigDecimal pretaxcost = BigDecimal.ZERO;
        BigDecimal taxedcost = BigDecimal.ZERO;
        if (serviceFund1 == null) {
            throw new BaseException("未找到费用申请单,ID=" + fundid);
        }
        // æ˜Žç»† tax_time ç»Ÿä¸€ä½¿ç”¨æœ¬æ¬¡ç®—税时间
        if (taxTime == null) {
            taxTime = new Date();
        }
        //创建一个用于存放“需要更新到数据库的ServiceFunddetail对象”的列表
        // 4. æœ¬æ‰¹æ¬¡åˆè®¡ï¼ˆå…¨éƒ¨ç”¨ BigDecimal,禁止 double ç´¯åŠ ï¼‰
        BigDecimal pretaxTotal = BigDecimal.ZERO;
        BigDecimal vatTotal = BigDecimal.ZERO;
        BigDecimal cityTaxTotal = BigDecimal.ZERO;
        BigDecimal laborPretaxTotal = BigDecimal.ZERO;
        BigDecimal taxTotal = BigDecimal.ZERO;
        BigDecimal taxedTotal = BigDecimal.ZERO;
        List<ServiceFunddetail> updateList = new ArrayList<>();
        // 5. éåŽ†æ¯æ¡æ˜Žç»†ï¼Œåˆ†æƒ…å†µå¤„ç†
        // 5. éåŽ†æ¯æ¡æ˜Žç»†
        for (ServiceFunddetail detail : serviceFunddetails) {
            // 5.1 èº«ä»½è¯å·ä¸ºç©ºï¼Œç›´æŽ¥èµ‹å€¼ï¼ˆä¸€äº›äººæˆ–公司,不需要算税)
            if (detail.getAmount() == null) {
                detail.setAmount(0.0);
            }
            if (detail.getTaxedamount() == null) {
                detail.setTaxedamount(0.0);
            }
            // æœåŠ¡è¯´æ˜Žä¸ºç©ºæŒ‰â€œéžç¨ŽåŽâ€å¤„ç†
            String scopeName = detail.getServicesscopename() == null ? "" : detail.getServicesscopename().trim();
            boolean afterTax = scopeName.contains("税后");
            // 5.1 èº«ä»½è¯å·ä¸ºç©ºï¼šä¸æ‰£å¢žå€¼ç¨Ž/城建税/劳务个税
            if (StringUtils.isEmpty(detail.getIdcardno())) {
                if (StringUtils.isEmpty(detail.getServicesscopename()) || !detail.getServicesscopename().contains("税后")) {
                    // æ™®é€šæ˜Žç»†ï¼Œç¨ŽåŽé‡‘额=申请金额
                    detail.setTaxedamount(detail.getAmount());
                } else {
                    // ç¨ŽåŽæ˜Žç»†ï¼Œç”³è¯·é‡‘额=税后金额
                    detail.setAmount(detail.getTaxedamount());
                }
                // ç´¯è®¡æœ¬æ‰¹æ¬¡æ€»é‡‘额
                pretaxcost = pretaxcost.add(BigDecimal.valueOf(detail.getAmount()));
                taxedcost = taxedcost.add(BigDecimal.valueOf(detail.getTaxedamount()));
                BigDecimal p = afterTax ? toMoney(detail.getTaxedamount()) : toMoney(detail.getAmount());
                detail.setAmount(p.doubleValue());
                detail.setVatAmount(0.0);
                detail.setCityTaxAmount(0.0);
                detail.setLaborPretaxAmount(p.doubleValue());
                detail.setTaxamount(0.0);
                detail.setTaxedamount(p.doubleValue());
                detail.setTaxVersion(VatTaxUtils.TAX_VERSION_V2);
                detail.setTaxTime(taxTime);
                updateList.add(detail);
                pretaxTotal = pretaxTotal.add(p);
                laborPretaxTotal = laborPretaxTotal.add(p);
                taxedTotal = taxedTotal.add(p);
                continue;
            }
            // 5.2 èº«ä»½è¯å·ä¸ä¸ºç©ºï¼Œéœ€æŸ¥åŽ†å²ç´¯è®¡é‡‘é¢ã€åºå·
            // 5.2 èº«ä»½è¯å·ä¸ä¸ºç©ºï¼šæœ¬æœˆç´¯è®¡ + åˆ†æ¡£ç®—税
            String idcard = detail.getIdcardno().trim();
            // 5.2.1 æŸ¥è¯¢æœ¬æœˆç´¯è®¡é‡‘额(缓存避免重复查库)
            TaxMoneySumEO taxSum = tempTaxSumMap.getOrDefault(idcard, null);
            // 5.2.1 å–本月累计(缓存避免重复查库)
            TaxMoneySumEO taxSum = tempTaxSumMap.get(idcard);
            if (taxSum == null) {
                taxSum = new TaxMoneySumEO();
                taxSum.setXh(1L);
                detail.setXh(1L);
                taxSum.setXh(0L);
                tempTaxSumMap.put(idcard, taxSum);
                detail.setAllAmount(taxSum.getAmounts());
            }
            long xh = (taxSum.getXh() == null ? 0L : taxSum.getXh()) + 1;
            taxSum.setXh(xh);
            detail.setXh(xh);
            // ç´¯è®¡ç¨Žå‰é‡‘额(不含本笔),用于页面展示
            detail.setAllAmount(taxSum.getAmounts());
            BigDecimal prevAmount = toMoney(taxSum.getAmounts());
            BigDecimal prevLaborPretax = toMoney(taxSum.getLaborPretaxAmounts());
            BigDecimal prevTax = toMoney(taxSum.getTaxAmounts());
            BigDecimal prevTaxed = toMoney(taxSum.getTaxedAmounts());
            BigDecimal prevVat = toMoney(taxSum.getVatAmounts());
            BigDecimal prevCityTax = toMoney(taxSum.getCityTaxAmounts());
            BigDecimal pretax;
            BigDecimal vat;
            BigDecimal cityTax;
            BigDecimal laborPretax;
            BigDecimal taxAmount;
            BigDecimal taxedAmount;
            if (afterTax) {
                // 5.2.2 ç¨ŽåŽå½•入:先反算劳务税前,再循环反解税前/增值税/城建税
                taxedAmount = toMoney(detail.getTaxedamount());
                BigDecimal sumTaxed = prevTaxed.add(taxedAmount);
                BigDecimal sumLaborPretax = new BigDecimal(TaxtUtils.getTaxationBefore(sumTaxed));
                laborPretax = VatTaxUtils.roundMoney(sumLaborPretax.subtract(prevLaborPretax));
                if (laborPretax.signum() < 0) {
                    // é€ç¬”向上取整可能带来几分钱的反算误差,容差内按 0 å¤„理;超出容差说明数据异常
                    if (laborPretax.abs().compareTo(new BigDecimal("0.05")) <= 0) {
                        log.warn("税后金额反算劳务税前出现负尾差,已按 0 å¤„理。人员:{},反算劳务税前={},原累计劳务税前={}",
                                safeName(detail), sumLaborPretax, prevLaborPretax);
                        laborPretax = BigDecimal.ZERO;
                    } else {
                        throw new BaseException("税后金额反算劳务税前失败,人员:" + safeName(detail));
                    }
                }
                taxAmount = VatTaxUtils.roundMoney(laborPretax.subtract(taxedAmount));
                if (taxAmount.signum() < 0) {
                    taxAmount = BigDecimal.ZERO;
                }
                VatTaxUtils.GrossAmount gross = VatTaxUtils.solveGross(laborPretax);
                pretax = gross.getPretax();
                vat = gross.getVat();
                cityTax = gross.getCityTax();
            } else {
                long xh = taxSum.getXh() + 1;
                taxSum.setXh(xh);
                tempTaxSumMap.put(idcard, taxSum);
                detail.setXh(xh);
                detail.setAllAmount(taxSum.getAmounts());
                // 5.2.3 ç¨Žå‰å½•入:先算增值税/城建税/劳务税前,再按累计劳务税前算劳务个税
                pretax = toMoney(detail.getAmount());
                vat = VatTaxUtils.calcVat(pretax);
                cityTax = VatTaxUtils.calcCityTax(vat);
                laborPretax = VatTaxUtils.roundMoney(pretax.subtract(vat).subtract(cityTax));
                BigDecimal sumLaborPretax = prevLaborPretax.add(laborPretax);
                BigDecimal sumTax = new BigDecimal(TaxtUtils.getTaxation(sumLaborPretax));
                taxAmount = VatTaxUtils.roundMoney(sumTax.subtract(prevTax));
                if (taxAmount.signum() < 0) {
                    taxAmount = BigDecimal.ZERO;
                }
                taxedAmount = VatTaxUtils.roundMoney(laborPretax.subtract(taxAmount));
            }
            // 5.2.3 ç®—税逻辑
            if (detail.getServicesscopename().contains("税后")) {
                // ç¨ŽåŽæ˜Žç»†ï¼šç´¯è®¡ç¨ŽåŽé‡‘额,反推税前金额和税金
//                if (idcard.equals("362329199103224511")) {
//                    log.info("朱玮安");
//                }
                BigDecimal newTaxedAmounts = BigDecimal.valueOf(taxSum.getTaxedAmounts()).add(BigDecimal.valueOf(detail.getTaxedamount()));
                String taxationBefore = TaxtUtils.getTaxationBefore(newTaxedAmounts);
                BigDecimal amountNow = new BigDecimal(taxationBefore).subtract(BigDecimal.valueOf(taxSum.getAmounts()));
                BigDecimal tax = amountNow.subtract(BigDecimal.valueOf(detail.getTaxedamount()));
                detail.setAmount(amountNow.doubleValue());
                detail.setTaxamount(tax.doubleValue());
                if (ObjectUtils.isEmpty(serviceFund1.getFundtaxtime())) {
                    detail.setTaxTime(new Date());
                } else {
                    detail.setTaxTime(serviceFund1.getFundtaxtime());
                }
                taxSum.setAmounts((BigDecimal.valueOf(taxSum.getAmounts()).add(amountNow)).doubleValue());
                taxSum.setTaxAmounts((BigDecimal.valueOf(taxSum.getTaxAmounts()).add(tax)).doubleValue());
                taxSum.setTaxedAmounts(newTaxedAmounts.doubleValue());
            } else {
                // ç¨Žå‰æ˜Žç»†ï¼šç´¯è®¡ç¨Žå‰é‡‘额,算总税金,再算本次税金和税后金额
                BigDecimal newAmounts = BigDecimal.valueOf(taxSum.getAmounts()).add(BigDecimal.valueOf(detail.getAmount()));
//                if (idcard.equals("362329199103224511")) {
//                    log.info("朱玮安");
//                }
                String taxation = TaxtUtils.getTaxation(newAmounts);
                BigDecimal taxAmountNow = new BigDecimal(taxation).subtract(new BigDecimal(String.valueOf(taxSum.getTaxAmounts())));
                BigDecimal texdAfterNow = BigDecimal.valueOf(detail.getAmount()).subtract(taxAmountNow);
                detail.setTaxamount(taxAmountNow.doubleValue());
                detail.setTaxedamount(texdAfterNow.doubleValue());
                if (ObjectUtils.isEmpty(serviceFund1.getFundtaxtime())) {
                    detail.setTaxTime(new Date());
                } else {
                    detail.setTaxTime(serviceFund1.getFundtaxtime());
                }
                taxSum.setAmounts(newAmounts.doubleValue());
                taxSum.setTaxAmounts((BigDecimal.valueOf(taxSum.getTaxAmounts()).add(taxAmountNow).doubleValue()));
                taxSum.setTaxedAmounts((BigDecimal.valueOf(taxSum.getTaxedAmounts()).add(texdAfterNow)).doubleValue());
            }
            detail.setAmount(pretax.doubleValue());
            detail.setVatAmount(vat.doubleValue());
            detail.setCityTaxAmount(cityTax.doubleValue());
            detail.setLaborPretaxAmount(laborPretax.doubleValue());
            detail.setTaxamount(taxAmount.doubleValue());
            detail.setTaxedamount(taxedAmount.doubleValue());
            detail.setTaxVersion(VatTaxUtils.TAX_VERSION_V2);
            detail.setTaxTime(taxTime);
            updateList.add(detail);
            pretaxcost = pretaxcost.add(BigDecimal.valueOf(detail.getAmount()));
            taxedcost = taxedcost.add(BigDecimal.valueOf(detail.getTaxedamount()));
            // 5.2.4 æ›´æ–°æœ¬æœˆç´¯è®¡
            taxSum.setAmounts(prevAmount.add(pretax).doubleValue());
            taxSum.setVatAmounts(prevVat.add(vat).doubleValue());
            taxSum.setCityTaxAmounts(prevCityTax.add(cityTax).doubleValue());
            taxSum.setLaborPretaxAmounts(prevLaborPretax.add(laborPretax).doubleValue());
            taxSum.setTaxAmounts(prevTax.add(taxAmount).doubleValue());
            taxSum.setTaxedAmounts(prevTaxed.add(taxedAmount).doubleValue());
            pretaxTotal = pretaxTotal.add(pretax);
            vatTotal = vatTotal.add(vat);
            cityTaxTotal = cityTaxTotal.add(cityTax);
            laborPretaxTotal = laborPretaxTotal.add(laborPretax);
            taxTotal = taxTotal.add(taxAmount);
            taxedTotal = taxedTotal.add(taxedAmount);
        }
        // 6. æ‰¹é‡æ›´æ–°æ‰€æœ‰æ˜Žç»†
        for (ServiceFunddetail d : updateList) {
            this.updateById(d);
        }
        // 7. æ›´æ–°ä¸»è¡¨ServiceFund
        ServiceFund serviceFund = new ServiceFund();
        serviceFund.setPretaxcost(pretaxcost.doubleValue());
        serviceFund.setTaxedcost(taxedcost.doubleValue());
        serviceFund.setId(serviceFunddetails.get(0).getFundid());
        serviceFund.setFundtaxtime(new Date());
        // 7. æ›´æ–°ä¸»è¡¨ service_fund çš„ 6 ä¸ªé‡‘额、算税时间、状态、版本
        ServiceFund serviceFund = new ServiceFund();
        serviceFund.setId(fundid);
        serviceFund.setPretaxcost(pretaxTotal.doubleValue());
        serviceFund.setVatCost(vatTotal.doubleValue());
        serviceFund.setCityTaxCost(cityTaxTotal.doubleValue());
        serviceFund.setLaborPretaxCost(laborPretaxTotal.doubleValue());
        serviceFund.setTaxcost(taxTotal.doubleValue());
        serviceFund.setTaxedcost(taxedTotal.doubleValue());
        serviceFund.setTaxVersion(VatTaxUtils.TAX_VERSION_V2);
        serviceFund.setFundtaxtime(taxTime);
        serviceFund.setIstax(1);
        serviceFundMapper.updateById(serviceFund);
        return true;
    }
    /**
     * null å®‰å…¨çš„金额转换
     */
    private static BigDecimal toMoney(Double value) {
        return value == null ? BigDecimal.ZERO : BigDecimal.valueOf(value);
    }
    /**
     * æŠ¥é”™æ—¶èƒ½å®šä½åˆ°å…·ä½“人员
     */
    private static String safeName(ServiceFunddetail detail) {
        String name = detail.getBeneficiaryname() == null ? "" : detail.getBeneficiaryname();
        String idcard = detail.getIdcardno() == null ? "" : detail.getIdcardno();
        return name + "(" + idcard + ")";
    }
//    @Override
@@ -493,6 +584,10 @@
        //将计算出来的数据放到serviceFunddetail中
        serviceFunddetail.setTaxamount(nowSingleTax.doubleValue());
        serviceFunddetail.setTaxedamount(taxAfter.doubleValue());
        // ç»©æ•ˆä¸å±žäºŽä¸“家费:不计算增值税/城建税,劳务税前 = ç¨Žå‰é‡‘额
        serviceFunddetail.setVatAmount(0.0);
        serviceFunddetail.setCityTaxAmount(0.0);
        serviceFunddetail.setLaborPretaxAmount(serviceFunddetail.getAmount());
        serviceFunddetail.setJxrq(nowMonth);
        save(serviceFunddetail);
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFundtaxServiceImpl.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.tax.VatTaxUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.domain.ServiceFund;
import com.ruoyi.project.domain.ServiceFunddetail;
@@ -23,6 +24,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.TemporalAdjusters;
@@ -171,66 +173,128 @@
    /**
     * æ‰¹é‡ç®—税主入口
     * 1. æ ¡éªŒå‚æ•°
     * 2. æ ¡éªŒæ‰¹æ¬¡åˆæ³•性(如有必要)
     * 3. æŸ¥è¯¢æœ¬æ‰¹æ¬¡æ‰€æœ‰ServiceFund(按id升序)
     * 4. é¢„查询本月所有身份证号的累计金额,缓存到Map,提升后续算税效率
     * 5. éåŽ†æ¯ä¸ªServiceFund,批量算税
     * 2. åªå…è®¸æœ€æ–°æ‰¹æ¬¡ç®—税
     * 3. å¯¹å½“前批次 service_fundtax åŠ è¡Œé”ï¼Œé˜²å¹¶å‘é‡å¤ç®—ç¨Ž
     * 4. æŸ¥è¯¢æœ¬æ‰¹æ¬¡æ‰€æœ‰ ServiceFund(按 id å‡åºï¼‰ï¼Œå¹¶é‡ç½® istax
     * 5. é¢„查询本月所有身份证号的累计金额,缓存到 Map
     * 6. æ ¡éªŒå½“月是否存在历史口径(v1)算税记录,防止新旧口径混算
     * 7. éåŽ†æ¯ä¸ª ServiceFund é€å•算税,汇总后回写 service_fundtax
     * <p>
     * æ•´æ‰¹åœ¨ä¸€ä¸ªäº‹åŠ¡å†…å®Œæˆï¼šä»»ä¸€æ¡å¤±è´¥åˆ™æ•´æ‰¹å›žæ»šï¼Œä¸ä¼šå‡ºçŽ°åŠæ‰¹æ•°æ®ã€‚
     *
     * @param fundTaxVO æ‰¹æ¬¡ç®—税参数
     * @return æ˜¯å¦ç®—税成功
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean batchFundTax(FundTaxVO fundTaxVO) {
        log.info("批量算税的fundTaxVO入参为:{}", fundTaxVO);
        // 1. æ ¡éªŒå‚æ•°
        if (fundTaxVO.getFundTaxId() == null) {
            throw new BaseException("分批算税出问题了,请检查后再进行计算");
        }
        // 2. æ ¡éªŒæ‰¹æ¬¡åˆæ³•性(仅限修改操作)
        // 2. åªå…è®¸æœ€æ–°æ‰¹æ¬¡ç®—税
        if (fundTaxVO.getAddOrupdate() == 1) {
            Integer maxFundTaxId = serviceFundtaxMapper.getMaxFundTaxId();
            log.info("批量算税的maxFundTaxId为:{}", maxFundTaxId);
            if (!fundTaxVO.getFundTaxId().equals(Long.valueOf(maxFundTaxId))) {
            if (maxFundTaxId == null || !fundTaxVO.getFundTaxId().equals(Long.valueOf(maxFundTaxId))) {
                throw new BaseException("该批数据不能算税");
            }
        }
        // 3. æŸ¥è¯¢æœ¬æ‰¹æ¬¡æ‰€æœ‰ServiceFund(按id升序)
        // 3. å¯¹å½“前批次汇总单加行锁(事务内有效),防止并发重复算税
        ServiceFundtax fundtax = serviceFundtaxMapper.selectByIdForUpdate(fundTaxVO.getFundTaxId());
        if (fundtax == null) {
            throw new BaseException("未找到算税汇总单,ID=" + fundTaxVO.getFundTaxId());
        }
        // 4. æŸ¥è¯¢æœ¬æ‰¹æ¬¡æ‰€æœ‰ ServiceFund(按 id å‡åºï¼‰
        ServiceFund serviceFund = new ServiceFund();
        serviceFund.setFundTaxId(fundTaxVO.getFundTaxId());
        serviceFund.setDel_flag(0);
        List<ServiceFund> serviceFunds = serviceFundService.selectServiceFundList(serviceFund);
        for (ServiceFund serviceFund1 : serviceFunds) {
            serviceFundMapper.updateFundIsTexById(serviceFund1.getId(), 0L);
        }
        // æŒ‰id升序排序,保证处理顺序一致
        serviceFunds.sort(Comparator.comparing(ServiceFund::getId));
        if (CollectionUtils.isEmpty(serviceFunds)) {
            log.warn("未查询到相关ServiceFund,fundTaxId={}", fundTaxVO.getFundTaxId());
            return false;
        }
        // é‡ç½®æœ¬æ‰¹ istax ä¸Žç®—税时间,使本月累计查询不会重复计入本批
        for (ServiceFund serviceFund1 : serviceFunds) {
            serviceFundMapper.updateFundIsTexById(serviceFund1.getId(), 0L);
        }
        // æŒ‰ id å‡åºæŽ’序,保证处理顺序一致
        serviceFunds.sort(Comparator.comparing(ServiceFund::getId));
        // 4. é¢„查询本月所有身份证号的累计金额,缓存到Map,提升后续算税效率
        TaxMoneyByItemEO taxMoneyVO = new TaxMoneyByItemEO();
        // 5. é¢„查询本月所有身份证号的累计金额,缓存到 Map,提升后续算税效率
        Date taxTime = new Date();
        Date firstDay = Date.from(LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay(ZoneId.systemDefault()).toInstant());
        TaxMoneyByItemEO taxMoneyVO = new TaxMoneyByItemEO();
        taxMoneyVO.setFirstDay(firstDay);
        taxMoneyVO.setTaxTime(new Date());
        taxMoneyVO.setTaxTime(taxTime);
        Map<String, TaxMoneySumEO> tempTaxSumMap = new HashMap<>();
        List<TaxMoneySumEO> taxSum = serviceFunddetailMapper.getTaxSum(taxMoneyVO);
        for (TaxMoneySumEO taxMoneySumEO : taxSum) {
            tempTaxSumMap.put(taxMoneySumEO.getIDCardNo(), taxMoneySumEO);
        if (CollectionUtils.isNotEmpty(taxSum)) {
            for (TaxMoneySumEO taxMoneySumEO : taxSum) {
                tempTaxSumMap.put(taxMoneySumEO.getIDCardNo(), taxMoneySumEO);
                // 6. å½“月已存在的历史口径(v1)记录会让累计基数新旧混算,直接阻止
                if (taxMoneySumEO.getV1Count() != null && taxMoneySumEO.getV1Count() > 0) {
                    throw new BaseException("当月已存在旧口径(v1)算税记录,为避免新旧口径混算,"
                            + "本月请继续使用旧口径或从下个月起再按新口径算税。人员身份证号:"
                            + taxMoneySumEO.getIDCardNo());
                }
            }
        }
        // 5. éåŽ†æ¯ä¸ªServiceFund,批量算税
        // 7. éåŽ†æ¯ä¸ª ServiceFund,逐单算税并汇总
        BigDecimal pretaxTotal = BigDecimal.ZERO;
        BigDecimal vatTotal = BigDecimal.ZERO;
        BigDecimal cityTaxTotal = BigDecimal.ZERO;
        BigDecimal laborPretaxTotal = BigDecimal.ZERO;
        BigDecimal taxTotal = BigDecimal.ZERO;
        BigDecimal taxedTotal = BigDecimal.ZERO;
        for (ServiceFund serviceFund1 : serviceFunds) {
            ServiceFunddetail serviceFunddetail = new ServiceFunddetail();
            serviceFunddetail.setFundid(serviceFund1.getId());
            List<ServiceFunddetail> serviceFunddetails = serviceFunddetailMapper.selectServiceFunddetailList(serviceFunddetail);
            // ä¼ å…¥ç´¯è®¡é‡‘额缓存,提升算税效率
            serviceFunddetailService.calculateTax(serviceFunddetails, tempTaxSumMap);
            if (CollectionUtils.isEmpty(serviceFunddetails)) {
                throw new BaseException("单据没有可算税的明细,单据ID=" + serviceFund1.getId()
                        + ",捐献案例=" + serviceFund1.getDonorname());
            }
            // ä¼ å…¥ç´¯è®¡é‡‘额缓存与统一算税时间
            serviceFunddetailService.calculateTax(serviceFunddetails, tempTaxSumMap, taxTime);
            for (ServiceFunddetail detail : serviceFunddetails) {
                pretaxTotal = pretaxTotal.add(toMoney(detail.getAmount()));
                vatTotal = vatTotal.add(toMoney(detail.getVatAmount()));
                cityTaxTotal = cityTaxTotal.add(toMoney(detail.getCityTaxAmount()));
                laborPretaxTotal = laborPretaxTotal.add(toMoney(detail.getLaborPretaxAmount()));
                taxTotal = taxTotal.add(toMoney(detail.getTaxamount()));
                taxedTotal = taxedTotal.add(toMoney(detail.getTaxedamount()));
            }
        }
        // 8. å›žå†™æ±‡æ€»å• service_fundtax(6 ä¸ªé‡‘额 + ç®—税状态 + ç‰ˆæœ¬ï¼‰
        ServiceFundtax updateFundtax = new ServiceFundtax();
        updateFundtax.setId(fundTaxVO.getFundTaxId());
        updateFundtax.setPretaxamount(VatTaxUtils.roundMoney(pretaxTotal));
        updateFundtax.setVatAmount(VatTaxUtils.roundMoney(vatTotal));
        updateFundtax.setCityTaxAmount(VatTaxUtils.roundMoney(cityTaxTotal));
        updateFundtax.setLaborPretaxAmount(VatTaxUtils.roundMoney(laborPretaxTotal));
        updateFundtax.setTaxamount(VatTaxUtils.roundMoney(taxTotal));
        updateFundtax.setTaxMoney(VatTaxUtils.roundMoney(taxTotal));
        updateFundtax.setTaxedamount(VatTaxUtils.roundMoney(taxedTotal));
        updateFundtax.setTaxVersion(VatTaxUtils.TAX_VERSION_V2);
        updateFundtax.setIstax(1);
        updateFundtax.setTaxedtime(taxTime);
        serviceFundtaxMapper.updateById(updateFundtax);
        log.info("批量算税完成,fundTaxId={},税前={},增值税={},城建税={},劳务税前={},劳务个税={},劳务税后={}",
                fundTaxVO.getFundTaxId(), pretaxTotal, vatTotal, cityTaxTotal, laborPretaxTotal, taxTotal, taxedTotal);
        return true;
    }
    /**
     * null å®‰å…¨çš„金额转换
     */
    private static BigDecimal toMoney(Double value) {
        return value == null ? BigDecimal.ZERO : BigDecimal.valueOf(value);
    }
}
ruoyi-project/src/main/resources/mapper/project/CasePanoramaMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
<?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.project.mapper.CasePanoramaMapper">
    <sql id="casePanoramaColumns">
        id,
        DonorName AS donorName,
        ReportTime AS reportTime,
        deptname AS deptName,
        ReporterName AS reporterName,
        Dontiontermination AS terminationFlag,
        workflow AS workflow,
        COALESCE(dd.dict_label, CAST(workflow AS CHAR)) AS workflowName,
        DonateTime AS donateTime,
        DonationHospital AS donationHospital,
        OrganName AS organName,
        OrganProcurement AS organProcurement,
        OrganTransplantation AS organTransplantation,
        COALESCE(DonorFamilysCost, 0) + COALESCE(medCost, 0) + COALESCE(ExpertCost, 0) + COALESCE(ItemCode3_4, 0) AS totalCost,
        DonorFamilysCost AS donorFamilyCost,
        medCost AS medCost,
        ExpertCost AS expertCost,
        ItemCode3_4 AS itemCode34,
        ItemCode42 AS itemCode42,
        ItemCode46 AS itemCode46,
        ItemCode50 AS itemCode50,
        ItemCode44 AS itemCode44,
        ItemCode47 AS itemCode47,
        ItemCode48 AS itemCode48,
        ItemCode21 AS itemCode21,
        ItemCode12 AS itemCode12,
        ItemCode17 AS itemCode17,
        ItemCode23 AS itemCode23,
        ItemCode25 AS itemCode25
    </sql>
    <select id="selectCasePanoramaList" resultType="com.ruoyi.project.domain.CasePanorama">
        SELECT
        <include refid="casePanoramaColumns"/>
        FROM v_donationfeestat v
        LEFT JOIN (
            SELECT dict_value, MAX(dict_label) AS dict_label
            FROM sys_dict_data
            WHERE dict_type = 'sys_donornode'
            GROUP BY dict_value
        ) dd ON dd.dict_value COLLATE utf8mb4_general_ci = CAST(v.workflow AS CHAR) COLLATE utf8mb4_general_ci
        <where>
            <if test="req.donorName != null and req.donorName != ''">
                AND DonorName LIKE CONCAT('%', #{req.donorName}, '%')
            </if>
            <if test="req.donateTimeBegin != null and req.donateTimeBegin != ''">
                AND DonateTime &gt;= #{req.donateTimeBegin}
            </if>
            <if test="req.donateTimeEnd != null and req.donateTimeEnd != ''">
                AND DonateTime &lt;= #{req.donateTimeEnd}
            </if>
            <if test="req.deptName != null and req.deptName != ''">
                AND deptname = #{req.deptName}
            </if>
            <if test="req.reporterName != null and req.reporterName != ''">
                AND ReporterName LIKE CONCAT('%', #{req.reporterName}, '%')
            </if>
        </where>
        ORDER BY ReportTime DESC, id DESC
    </select>
    <select id="selectDeptNames" resultType="java.lang.String">
        SELECT DISTINCT deptname
        FROM v_donationfeestat
        WHERE deptname IS NOT NULL AND deptname != ''
        ORDER BY deptname
    </select>
</mapper>
ruoyi-project/src/main/resources/mapper/project/ServiceFundMapper.xml
@@ -51,6 +51,10 @@
        <result property="uploadtime" column="uploadtime"/>
        <result property="flowlevel" column="FlowLevel"/>
        <result property="pretaxcost" column="PretaxCost"/>
        <result property="vatCost" column="vat_cost"/>
        <result property="cityTaxCost" column="city_tax_cost"/>
        <result property="laborPretaxCost" column="labor_pretax_cost"/>
        <result property="taxVersion" column="tax_version"/>
        <result property="taxedcost" column="TaxedCost"/>
        <result property="backflowlevel" column="backFlowLevel"/>
        <result property="zffs" column="zffs"/>
@@ -87,6 +91,10 @@
               UnitNo,
               UnitUserNo,
               PretaxCost,
               vat_cost,
               city_tax_cost,
               labor_pretax_cost,
               tax_version,
               TaxedCost,
               Phone,
               AnnexBankCard,
ruoyi-project/src/main/resources/mapper/project/ServiceFunddetailMapper.xml
@@ -37,6 +37,10 @@
        <result property="amount" column="Amount"/>
        <result property="taxamount" column="TaxAmount"/>
        <result property="taxedamount" column="TaxedAmount"/>
        <result property="vatAmount" column="vat_amount"/>
        <result property="cityTaxAmount" column="city_tax_amount"/>
        <result property="laborPretaxAmount" column="labor_pretax_amount"/>
        <result property="taxVersion" column="tax_version"/>
        <result property="prepaidamount" column="PrepaidAmount"/>
        <result property="invoicecount" column="InvoiceCount"/>
        <result property="attachcount" column="AttachCount"/>
@@ -108,6 +112,10 @@
               TaxAmount,
               InvoiceCount,
               TaxedAmount,
               vat_amount,
               city_tax_amount,
               labor_pretax_amount,
               tax_version,
               AttachCount,
               Quantity,
               Price,
@@ -232,6 +240,10 @@
        a.TaxAmount,
        a.InvoiceCount,
        a.TaxedAmount,
        a.vat_amount,
        a.city_tax_amount,
        a.labor_pretax_amount,
        a.tax_version,
        a.AttachCount,
        a.Quantity,
        a.Price,
@@ -281,7 +293,15 @@
    <select id="getTaxSum" parameterType="com.ruoyi.project.domain.vo.TaxMoneyByItemEO"
            resultType="com.ruoyi.project.domain.vo.TaxMoneySumEO">
        SELECT sum(a.Amount) amounts, sum(a.TaxAmount) taxAmounts, sum(a.TaxedAmount) taxedAmounts, max(a.xh) as xh,a.IDCardNo
        SELECT sum(a.Amount)                                                     amounts,
               sum(COALESCE(a.labor_pretax_amount, a.Amount))                     laborPretaxAmounts,
               sum(COALESCE(a.TaxAmount, 0))                                      taxAmounts,
               sum(COALESCE(a.TaxedAmount, 0))                                    taxedAmounts,
               sum(COALESCE(a.vat_amount, 0))                                     vatAmounts,
               sum(COALESCE(a.city_tax_amount, 0))                                cityTaxAmounts,
               sum(CASE WHEN a.tax_version = 'v1' THEN 1 ELSE 0 END)              v1Count,
               max(a.xh)                                                          xh,
               a.IDCardNo
        FROM service_funddetail a,
             service_fund b
        where a.FundID = b.id
ruoyi-project/src/main/resources/mapper/project/ServiceFunddetailSharedMapper.xml
@@ -37,6 +37,9 @@
        <result property="amount" column="Amount"/>
        <result property="taxamount" column="TaxAmount"/>
        <result property="taxedamount" column="TaxedAmount"/>
        <result property="vatAmount" column="vat_amount"/>
        <result property="cityTaxAmount" column="city_tax_amount"/>
        <result property="laborPretaxAmount" column="labor_pretax_amount"/>
        <result property="prepaidamount" column="PrepaidAmount"/>
        <result property="paidamount" column="paidAmount"/>
        <result property="paidtime" column="paidTime"/>
@@ -96,6 +99,9 @@
               Amount,
               TaxAmount,
               TaxedAmount,
               vat_amount,
               city_tax_amount,
               labor_pretax_amount,
               PrepaidAmount,
               paidAmount,
               paidTime,
ruoyi-project/src/main/resources/mapper/project/ServiceFundtaxMapper.xml
@@ -15,6 +15,10 @@
        <result property="managerno" column="ManagerNo"/>
        <result property="managername" column="ManagerName"/>
        <result property="pretaxamount" column="Pretaxamount"/>
        <result property="vatAmount" column="vat_amount"/>
        <result property="cityTaxAmount" column="city_tax_amount"/>
        <result property="laborPretaxAmount" column="labor_pretax_amount"/>
        <result property="taxVersion" column="tax_version"/>
        <result property="taxedamount" column="TaxedAmount"/>
        <result property="taxamount" column="TaxAmount"/>
        <result property="medicalcost" column="MedicalCost"/>
@@ -60,6 +64,10 @@
               Pretaxamount,
               TaxedAmount,
               TaxAmount,
               vat_amount,
               city_tax_amount,
               labor_pretax_amount,
               tax_version,
               MedicalCost,
               BodyMaintainCost,
               OrganGainCost,
@@ -143,4 +151,10 @@
        where del_flag = 0;
    </select>
    <select id="selectByIdForUpdate" resultMap="ServiceFundtaxResult">
        <include refid="selectServiceFundtaxVo"/>
        where ID = #{id}
        for update
    </select>
</mapper>