fix(settlement): 共用收款主体那格改写「共用收款主体(N 家)」,悬停列出名单
接着上一条去重:一格里列出 7 家商家的名字有 50 多个字符,在 180px 的列里截断之后等于看不出
是什么,只保证了"信息不丢"、没保证"看得懂"。改成格子只写「共用收款主体(N 家)」,名单随行返回
(merchantNames)由前端悬停列出——一眼可读,想知道具体哪几家悬停就有。只有一家时照旧显示那一家。
按商家搜索不受影响:筛选是在**数据库**那层做的(先定"查哪些商家的收款主体"再去问通道),不是拿
格子里的显示文本去匹配。所以共用主体照样搜得到;而且筛到只剩一家时那一格就显示这一家的名字——
运营按商家查的时候,看到的就是自己查的那家。这条行为用测试钉住了
(searchingByOneOfTheSharingMerchantsStillFindsTheSharedFlow)。
| | |
| | | return ({ pending: 'warning', approved: 'success', paid: 'success', rejected: 'danger', failed: 'danger', processing: 'primary' } as Record<string, 'warning' | 'success' | 'danger' | 'primary'>)[value] || 'primary' |
| | | } |
| | | |
| | | /** 斗拱结算流水的交易状态:S 成功、F 失败、P 处理中。 */ |
| | | function settleStatusLabel(value: unknown) { |
| | | /** |
| | | * 商家名称那一格的悬停内容:几家共用同一个汇付商户号时,把共用的商家都列出来。 |
| | | * 格子里只能写「共用收款主体(N 家)」——一条流水是这几家一起的,名字全塞进去放不下(7 家就有 50 多个字符)。 |
| | | */ |
| | | function settlementMerchantTip(row: Record<string, any>) { |
| | | const names: string[] = Array.isArray(row.merchantNames) ? row.merchantNames : [] |
| | | return names.length > 1 ? names.join('、') : String(row.merchantName || '') |
| | | } |
| | | |
| | | /** 斗拱结算流水的交易状态:S 成功、F 失败、P 处理中。 */function settleStatusLabel(value: unknown) { |
| | | return ({ S: '已结算', F: '结算失败', P: '处理中' } as Record<string, string>)[String(value || '')] || String(value || '-') |
| | | } |
| | | |
| | |
| | | <el-table v-loading="loading" :data="rows" border class="settlement-table" empty-text="暂无数据"> |
| | | <template v-if="kind === 'settlements'"> |
| | | <el-table-column prop="providerTransactionNo" label="结算流水号" min-width="190" show-overflow-tooltip /> |
| | | <el-table-column prop="merchantName" label="商家名称" min-width="180" show-overflow-tooltip /> |
| | | <!-- 几家商家共用一个收款主体时,格子写成「共用收款主体(N 家)」——一条流水是几家一起的, |
| | | 把名字全塞进这一格放不下(7 家就 50 多个字符),所以名单放到悬停提示里。 --> |
| | | <el-table-column label="商家名称" min-width="180"> |
| | | <template #default="{ row }"> |
| | | <el-tooltip placement="top" :content="settlementMerchantTip(row)"> |
| | | <span class="cell-ellipsis">{{ row.merchantName }}</span> |
| | | </el-tooltip> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="结算日期" min-width="118"><template #default="{ row }">{{ formatSettleDate(row.settleDate) }}</template></el-table-column> |
| | | <el-table-column label="结算金额" min-width="110" align="right"><template #default="{ row }">{{ formatAmount(row.settleAmountCents) }}</template></el-table-column> |
| | | <el-table-column label="手续费" min-width="96" align="right"><template #default="{ row }">{{ formatAmount(row.feeAmountCents) }}</template></el-table-column> |
| | |
| | | .adopt-alert { margin-bottom: 12px; } |
| | | .table-panel { margin-top: 16px; padding: 0 20px 12px; } |
| | | .settlement-table { width: 100%; } |
| | | /* 自绘 tooltip 的那一格自己截断:不再用列的 show-overflow-tooltip(否则会和自绘的提示打架)。 */ |
| | | .cell-ellipsis { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| | | .settlement-table :deep(th.el-table__cell) { color: #303133; font-weight: 600; background: #f7f8fa; } |
| | | .settlement-table :deep(.el-table__cell) { padding: 10px 0; } |
| | | .pagination-bar { display: flex; align-items: center; justify-content: flex-end; gap: 18px; padding-top: 14px; color: #606266; font-size: 13px; } |
| | |
| | | int limit = Math.min(Math.max(size, 1), HuifuSettlementRecordGateway.PROVIDER_MAX_PAGE_SIZE); |
| | | for (Map.Entry<String, List<Map<String, Object>>> entry : merchantsByHuifuId.entrySet()) { |
| | | String huifuId = entry.getKey(); |
| | | String label = sharedMerchantLabel(entry.getValue()); |
| | | List<String> names = merchantNames(entry.getValue()); |
| | | // 一格放不下几家商家的名字(7 家共用就是 50 多个字符),所以格子只写「共用收款主体(N 家)」, |
| | | // 具体是哪几家随行返回给前端做悬停提示——「信息不丢」和「一眼看得懂」两样都要。 |
| | | String label = names.size() == 1 ? names.get(0) : "共用收款主体(" + names.size() + " 家)"; |
| | | try { |
| | | HuifuSettlementRecordGateway.SettlementRecords response = |
| | | settlementRecordGateway.query(huifuId, beginDate, endDate, limit); |
| | |
| | | failures.add(label + ":" + response.message()); |
| | | continue; |
| | | } |
| | | for (Map<String, Object> record : decode(response.rawRecords(), entry.getValue().get(0), label)) { |
| | | for (Map<String, Object> record : decode(response.rawRecords(), entry.getValue().get(0), label, names)) { |
| | | if (matchesStatus(record, status)) records.add(record); |
| | | } |
| | | } catch (RuntimeException exception) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 这一行显示成哪家的。 |
| | | * 这一个商户号下、本次结果里涉及到的商家名(按商家 id 升序、去重)。 |
| | | * |
| | | * <p>一个商户号可以被多家商家共用(连锁各门店各是一个独立商家,共用一个收款主体),而流水属于 |
| | | * 商户号、不属于其中任何一家。只说第一家会误导,只留一个名字又像把别家漏了,所以共用时把商家名 |
| | | * 都列出来并注明共用;只有一家时就照常显示那一家。 |
| | | * <p>一个商户号可以被多家商家共用(连锁各门店各是一个独立商家,共用一个收款主体),流水属于商户号、 |
| | | * 不属于其中任何一家。所以格子写成「共用收款主体(N 家)」、名单随行带上给前端做悬停提示—— |
| | | * 只说第一家会误导,只留一个名字又像把别家漏了。 |
| | | */ |
| | | private static String sharedMerchantLabel(List<Map<String, Object>> merchants) { |
| | | List<String> names = merchants.stream().map(merchant -> String.valueOf(merchant.get("merchantName"))) |
| | | private static List<String> merchantNames(List<Map<String, Object>> merchants) { |
| | | return merchants.stream().map(merchant -> String.valueOf(merchant.get("merchantName"))) |
| | | .distinct().toList(); |
| | | return names.size() == 1 ? names.get(0) : String.join("、", names) + "(共用收款主体)"; |
| | | } |
| | | |
| | | /** 把通道返回的结算记录 JSON 转成后台要展示的字段;只读展示,不做金额换算以外的加工。 */ |
| | | private List<Map<String, Object>> decode(String rawRecords, Map<String, Object> merchant, String merchantLabel) { |
| | | private List<Map<String, Object>> decode(String rawRecords, Map<String, Object> merchant, String merchantLabel, |
| | | List<String> merchantNames) { |
| | | List<Map<String, Object>> decoded = new ArrayList<>(); |
| | | if (rawRecords == null || rawRecords.isBlank()) return decoded; |
| | | List<Map<String, Object>> parsed; |
| | |
| | | for (Map<String, Object> record : parsed) { |
| | | Map<String, Object> row = new LinkedHashMap<>(); |
| | | row.put("merchantId", merchant.get("merchantId")); |
| | | // 商家名取聚合出来的那个标签:几家共用同一个商户号时,这一条流水是所有共用商家一起的。 |
| | | // 商家名取聚合出来的那个标签:几家共用同一个商户号时,这一条流水是所有共用商家一起的; |
| | | // 名单一并带上,前端悬停时列出来(格子里放不下那么多字)。 |
| | | row.put("merchantName", merchantLabel); |
| | | row.put("merchantNames", merchantNames); |
| | | row.put("storeName", merchant.get("storeName")); |
| | | row.put("providerTransactionNo", record.get("trans_id")); |
| | | row.put("settleDate", record.get("trans_date")); |
| | |
| | | .as("一条流水只该出现一次,不能按共用它的商家数重复") |
| | | .containsExactlyInAnyOrder("TX-S-" + sharedHuifuId, "TX-F-" + sharedHuifuId); |
| | | verify(settlementRecordGateway, times(1)).query(eq(sharedHuifuId), any(), any(), anyInt()); |
| | | // 格子放不下几家商家的名字(7 家就 50 多个字符),所以显示成「共用收款主体(N 家)」, |
| | | // 名单随行带给前端做悬停提示。 |
| | | assertThat(recordsOf(fetch(null, null, null, null))) |
| | | .filteredOn(record -> String.valueOf(record.get("providerTransactionNo")).endsWith(sharedHuifuId)) |
| | | .isNotEmpty() |
| | | .allSatisfy(record -> { |
| | | assertThat(record).containsEntry("merchantName", "共用收款主体(2 家)"); |
| | | assertThat(record.get("merchantNames")) |
| | | .asInstanceOf(org.assertj.core.api.InstanceOfAssertFactories.list(String.class)) |
| | | .containsExactly("Settlement chain A " + suffix, "Settlement chain B " + suffix); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 按商家搜索照样能搜到共用的那条流水。 |
| | | * |
| | | * <p>筛选是在**数据库那一层**做的(先定"查哪些商家的收款主体",再去问通道),不是拿格子里的显示 |
| | | * 文本去匹配;所以格子改成「共用收款主体(N 家)」不会让搜索失效。而且筛到只剩一家时,那一格就 |
| | | * 显示这一家的名字——运营按商家查的时候,看到的就是自己查的那家。 |
| | | */ |
| | | @Test |
| | | void searchingByOneOfTheSharingMerchantsStillFindsTheSharedFlow() { |
| | | String sharedHuifuId = "TF" + suffix + "SEARCH"; |
| | | approveMerchantOn(sharedHuifuId, "Settlement search A " + suffix, "NORMAL", "1", true, "SEARCH-A", "餐饮服务", "13800000007"); |
| | | approveMerchantOn(sharedHuifuId, "Settlement search B " + suffix, "NORMAL", "1", true, "SEARCH-B", "餐饮服务", "13800000008"); |
| | | |
| | | List<Map<String, Object>> rows = recordsOf(fetch("Settlement search A " + suffix, null, null, null)); |
| | | |
| | | assertThat(rows) |
| | | .as("按商家筛的是「看谁名下的收款主体」,共用主体下也能搜到") |
| | | .filteredOn(record -> String.valueOf(record.get("providerTransactionNo")).endsWith(sharedHuifuId)) |
| | | .isNotEmpty() |
| | | .allSatisfy(record -> assertThat(record).containsEntry("merchantName", "Settlement search A " + suffix)); |
| | | verify(settlementRecordGateway).query(eq(sharedHuifuId), any(), any(), anyInt()); |
| | | } |
| | | |
| | | /** 关闭的子商户回的是通道原话;它只在真的被查到时才会成为一条失败提示。 */ |