sinake
2 天以前 e24ab2fc4d092b4599ef44ddce5ebdbbc0bd8f9a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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;
 
/**
 * 报销审批钉钉消息内容组装。
 *
 * <p>统一「标题 + 单据信息 + 审批意见文案」,供以下发送点复用:
 * <ul>
 *   <li>致远 OA 回传审批意见 —— {@code ServiceFundServiceImpl#processlog} → {@code sendMeg}</li>
 *   <li>系统内审核驳回(费用报销) —— {@code ServiceFundServiceImpl#checkFund}</li>
 *   <li>系统内审核驳回(差旅费) —— {@code ServiceReimbursementServiceImpl#checkFund}</li>
 *   <li>差旅费上报提交 —— {@code ServiceReimbursementController#travelexpensereport}</li>
 * </ul>
 *
 * <p><b>为什么要一行一个 map:</b>{@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<ConcurrentHashMap<String, Object>> newRows() {
        return new ArrayList<ConcurrentHashMap<String, Object>>();
    }
 
    /**
     * 追加一行。值为空(null / 空串 / 全空白)时**不追加**,避免钉钉里出现空行。
     */
    public static void addRow(List<ConcurrentHashMap<String, Object>> rows, String key, Object value) {
        if (rows == null || key == null || value == null) {
            return;
        }
        if (value instanceof String && StringUtils.isEmpty(((String) value).trim())) {
            return;
        }
        ConcurrentHashMap<String, Object> row = new ConcurrentHashMap<String, Object>(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);
    }
 
    /**
     * 追加报销单基础信息:报销单类型 / 报销人 / 金额 / 捐献案例 / 单据编号。
     *
     * <p>金额口径:费用类取 {@code service_fund.pretaxcost}(审核页「申请金额」列),
     * 差旅费取 {@code service_reimbursement.amountrequested}(审核页「报销金额」列)。
     */
    public static void addBillInfo(List<ConcurrentHashMap<String, Object>> 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<ConcurrentHashMap<String, Object>> rows,
                                     String travelers, String reason) {
        addRow(rows, KEY_TRAVELERS, travelers);
        addRow(rows, KEY_REASON, reason);
    }
 
    /**
     * 追加审批结果:审批人 / 审批时间 / 意见。
     *
     * <p>审批时间传 {@link Date}:键名含「审批时间」,由 {@code DingTalkServiceImpl} 统一格式化。
     */
    public static void addAuditResult(List<ConcurrentHashMap<String, Object>> 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<ConcurrentHashMap<String, Object>> 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 业务方确认)。
     *
     * <pre>
     *   通过(1)  → "审批通过。"   + 意见备注(若有)
     *   驳回(2)  → "审批未通过。" + 意见备注(若有)
     *   完结(3)  → "流程已完结。"
     *   其他/空  → 原样返回备注;无备注返回空串
     * </pre>
     *
     * <p>实测依据(本地库 {@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);
    }
}