package com.ruoyi.project.common; import com.ruoyi.common.enums.ApplyTypeEnum; import com.ruoyi.common.utils.StringUtils; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.ConcurrentHashMap; /** * 报销审批钉钉消息内容组装。 * *

统一「标题 + 单据信息 + 审批意见文案」,供以下发送点复用: *

* *

为什么要一行一个 map:{@code DingTalkReqVo.contents} 的元素是 * {@link ConcurrentHashMap},其遍历顺序不保证;一条 map 里塞多行会导致钉钉表单行序错乱。 * 这里每条信息单独一个「单键 map」,用 List 的顺序保证显示顺序。 * * @author ruoyi */ public final class FundNotifyMessageBuilder { /** 钉钉消息统一标题(2026-10-08 业务方确认统一为「OPO报销审批」) */ public static final String TITLE = "OPO报销审批"; /** 审批结论:通过 */ public static final String CONCLUSION_PASS = "1"; /** 审批结论:驳回 */ public static final String CONCLUSION_REJECT = "2"; /** 审批结论:完结 */ public static final String CONCLUSION_FINISH = "3"; private static final String KEY_TYPE = "报销单类型:"; private static final String KEY_BENEFICIARY = "报销人:"; private static final String KEY_AMOUNT = "金额:"; private static final String KEY_DONOR = "捐献案例:"; private static final String KEY_BILL_NO = "单据编号:"; private static final String KEY_TRAVELERS = "出差人:"; private static final String KEY_REASON = "出差事由:"; private static final String KEY_CHECKER = "审批人:"; private static final String KEY_CHECK_TIME = "审批时间:"; private static final String KEY_OPINION = "意见:"; private static final String KEY_SUBMITTER = "提交人:"; private static final String KEY_SUBMIT_TIME = "提交时间:"; private static final String KEY_CONTENT = "内容:"; private FundNotifyMessageBuilder() { } /** 新建内容行容器 */ public static List> newRows() { return new ArrayList>(); } /** * 追加一行。值为空(null / 空串 / 全空白)时**不追加**,避免钉钉里出现空行。 */ public static void addRow(List> rows, String key, Object value) { if (rows == null || key == null || value == null) { return; } if (value instanceof String && StringUtils.isEmpty(((String) value).trim())) { return; } ConcurrentHashMap row = new ConcurrentHashMap(1); row.put(key, value); rows.add(row); } /** 报销单类型:applytype → 中文名;未匹配返回 null(该行不显示) */ public static String typeName(String applytype) { return StringUtils.isEmpty(applytype) ? null : ApplyTypeEnum.getDescByCode(applytype); } /** 单据编号:优先取业务编号 bh,缺失时退回主键 id */ public static String billNo(String bh, Long id) { if (StringUtils.isNotEmpty(bh)) { return bh; } return id == null ? null : String.valueOf(id); } /** * 追加报销单基础信息:报销单类型 / 报销人 / 金额 / 捐献案例 / 单据编号。 * *

金额口径:费用类取 {@code service_fund.pretaxcost}(审核页「申请金额」列), * 差旅费取 {@code service_reimbursement.amountrequested}(审核页「报销金额」列)。 */ public static void addBillInfo(List> rows, String applytype, String beneficiary, Double amount, String donorname, String billNo) { addRow(rows, KEY_TYPE, typeName(applytype)); addRow(rows, KEY_BENEFICIARY, beneficiary); addRow(rows, KEY_AMOUNT, money(amount)); addRow(rows, KEY_DONOR, donorname); addRow(rows, KEY_BILL_NO, billNo); } /** 追加差旅费专有信息:出差人 / 出差事由 */ public static void addTravelInfo(List> rows, String travelers, String reason) { addRow(rows, KEY_TRAVELERS, travelers); addRow(rows, KEY_REASON, reason); } /** * 追加审批结果:审批人 / 审批时间 / 意见。 * *

审批时间传 {@link Date}:键名含「审批时间」,由 {@code DingTalkServiceImpl} 统一格式化。 */ public static void addAuditResult(List> rows, String checker, Date checkTime, String opinion) { addRow(rows, KEY_CHECKER, checker); addRow(rows, KEY_CHECK_TIME, checkTime); addRow(rows, KEY_OPINION, opinion); } /** 追加提交通知信息:提交人 / 提交时间 / 内容(时间在此预格式化,避免 Date.toString()) */ public static void addSubmitInfo(List> rows, String submitter, Date submitTime, String content) { addRow(rows, KEY_SUBMITTER, submitter); addRow(rows, KEY_SUBMIT_TIME, time(submitTime)); addRow(rows, KEY_CONTENT, content); } /** * 审批意见文案(2026-10-08 业务方确认)。 * *

     *   通过(1)  → "审批通过。"   + 意见备注(若有)
     *   驳回(2)  → "审批未通过。" + 意见备注(若有)
     *   完结(3)  → "流程已完结。"
     *   其他/空  → 原样返回备注;无备注返回空串
     * 
* *

实测依据(本地库 {@code service_fundflow.flowlevel=100} 即致远回传记录): * 通过 11618 条中 9606 条意见为空、完结 1457 条全部为空、驳回 743 条中 161 条为空。 * 若不给结论兜底文案,钉钉消息的「意见」一栏就是空白(这正是业务反馈的问题)。 */ public static String opinion(String conclusion, String content) { String remark = content == null ? "" : content.trim(); String base; if (CONCLUSION_PASS.equals(conclusion)) { base = "审批通过。"; } else if (CONCLUSION_REJECT.equals(conclusion)) { base = "审批未通过。"; } else if (CONCLUSION_FINISH.equals(conclusion)) { base = "流程已完结。"; } else { // 结论缺失:只显示备注,不臆测结论 return remark; } return remark.isEmpty() ? base : base + remark; } /** 金额显示:两位小数、无千分位,如 {@code 1200.00 元} */ public static String money(Double amount) { if (amount == null) { return null; } return new BigDecimal(String.valueOf(amount)) .setScale(2, RoundingMode.HALF_UP) .toPlainString() + " 元"; } /** 时间显示:{@code yyyy-MM-dd HH:mm:ss}(24 小时制) */ public static String time(Date date) { return date == null ? null : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); } }