|
@@ -0,0 +1,205 @@
|
|
|
|
|
+package com.fs.billing.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.fs.billing.vo.ModuleConsumptionVo;
|
|
|
|
|
+import com.fs.billing.service.ModuleConsumptionService;
|
|
|
|
|
+import com.fs.company.domain.Company;
|
|
|
|
|
+import com.fs.company.mapper.CompanyMapper;
|
|
|
|
|
+import com.fs.proxy.domain.ProxyTenantRel;
|
|
|
|
|
+import com.fs.proxy.service.ProxyTenantRelService;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 模块消费统计服务实现
|
|
|
|
|
+ * 从 tenant_wallet_txn(CONSUME) 按 biz_type 聚合消费金额
|
|
|
|
|
+ */
|
|
|
|
|
+@Service
|
|
|
|
|
+public class ModuleConsumptionServiceImpl implements ModuleConsumptionService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(ModuleConsumptionServiceImpl.class);
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private JdbcTemplate jdbcTemplate;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private CompanyMapper companyMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private ProxyTenantRelService proxyTenantRelService;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ModuleConsumptionVo reportForAdmin(String beginTime, String endTime, String tenantName) {
|
|
|
|
|
+ List<Long> tenantIds = null;
|
|
|
|
|
+ if (tenantName != null && !tenantName.isEmpty()) {
|
|
|
|
|
+ Company query = new Company();
|
|
|
|
|
+ query.setCompanyName(tenantName);
|
|
|
|
|
+ List<Company> companies = companyMapper.selectCompanyList(query);
|
|
|
|
|
+ tenantIds = companies.stream().map(Company::getCompanyId).collect(Collectors.toList());
|
|
|
|
|
+ if (tenantIds.isEmpty()) {
|
|
|
|
|
+ return buildEmptyReport();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return buildReport(tenantIds, beginTime, endTime);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ModuleConsumptionVo reportForProxy(Long proxyId, String beginTime, String endTime) {
|
|
|
|
|
+ List<ProxyTenantRel> rels = proxyTenantRelService.selectProxyTenantRelByProxyId(proxyId);
|
|
|
|
|
+ if (rels == null || rels.isEmpty()) {
|
|
|
|
|
+ return buildEmptyReport();
|
|
|
|
|
+ }
|
|
|
|
|
+ List<Long> tenantIds = rels.stream().map(ProxyTenantRel::getTenantId).distinct().collect(Collectors.toList());
|
|
|
|
|
+ return buildReport(tenantIds, beginTime, endTime);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ModuleConsumptionVo reportForCompany(Long tenantId, String beginTime, String endTime) {
|
|
|
|
|
+ return buildReport(Collections.singletonList(tenantId), beginTime, endTime);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private ModuleConsumptionVo buildReport(List<Long> tenantIds, String beginTime, String endTime) {
|
|
|
|
|
+ ModuleConsumptionVo vo = new ModuleConsumptionVo();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 按 biz_type 聚合
|
|
|
|
|
+ String sql = "SELECT COALESCE(t.biz_type, 'OTHER') as biz_type, " +
|
|
|
|
|
+ "COUNT(*) as txn_count, " +
|
|
|
|
|
+ "SUM(COALESCE(ABS(t.amount), 0)) as total_amount " +
|
|
|
|
|
+ "FROM tenant_wallet_txn t " +
|
|
|
|
|
+ "WHERE t.txn_type = 'CONSUME' ";
|
|
|
|
|
+ List<Object> params = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ if (tenantIds != null && !tenantIds.isEmpty()) {
|
|
|
|
|
+ sql += "AND t.tenant_id IN (" + tenantIds.stream().map(id -> "?").collect(Collectors.joining(",")) + ") ";
|
|
|
|
|
+ params.addAll(tenantIds);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (beginTime != null && !beginTime.isEmpty()) {
|
|
|
|
|
+ sql += "AND t.create_time >= ? ";
|
|
|
|
|
+ params.add(beginTime + " 00:00:00");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (endTime != null && !endTime.isEmpty()) {
|
|
|
|
|
+ sql += "AND t.create_time <= ? ";
|
|
|
|
|
+ params.add(endTime + " 23:59:59");
|
|
|
|
|
+ }
|
|
|
|
|
+ sql += "GROUP BY COALESCE(t.biz_type, 'OTHER') ORDER BY total_amount DESC";
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> moduleRows = jdbcTemplate.queryForList(sql, params.toArray());
|
|
|
|
|
+
|
|
|
|
|
+ // 构建模块明细
|
|
|
|
|
+ List<ModuleConsumptionVo.ModuleItem> moduleBreakdown = new ArrayList<>();
|
|
|
|
|
+ BigDecimal totalAmount = BigDecimal.ZERO;
|
|
|
|
|
+ long totalCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (Map<String, Object> row : moduleRows) {
|
|
|
|
|
+ String bizType = (String) row.get("biz_type");
|
|
|
|
|
+ BigDecimal amount = toBigDecimal(row.get("total_amount"));
|
|
|
|
|
+ Long count = toLong(row.get("txn_count"));
|
|
|
|
|
+
|
|
|
|
|
+ ModuleConsumptionVo.ModuleItem item = new ModuleConsumptionVo.ModuleItem();
|
|
|
|
|
+ item.setModuleCode(bizType);
|
|
|
|
|
+ item.setModuleName(ModuleConsumptionVo.MODULE_NAME_MAP.getOrDefault(bizType, bizType));
|
|
|
|
|
+ item.setTotalAmount(amount);
|
|
|
|
|
+ item.setCount(count);
|
|
|
|
|
+ moduleBreakdown.add(item);
|
|
|
|
|
+
|
|
|
|
|
+ totalAmount = totalAmount.add(amount);
|
|
|
|
|
+ totalCount += count;
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setModuleBreakdown(moduleBreakdown);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 按租户聚合
|
|
|
|
|
+ String tenantSql = "SELECT t.tenant_id, COALESCE(t.biz_type, 'OTHER') as biz_type, " +
|
|
|
|
|
+ "SUM(COALESCE(ABS(t.amount), 0)) as total_amount " +
|
|
|
|
|
+ "FROM tenant_wallet_txn t " +
|
|
|
|
|
+ "WHERE t.txn_type = 'CONSUME' ";
|
|
|
|
|
+ List<Object> tenantParams = new ArrayList<>();
|
|
|
|
|
+ if (tenantIds != null && !tenantIds.isEmpty()) {
|
|
|
|
|
+ tenantSql += "AND t.tenant_id IN (" + tenantIds.stream().map(id -> "?").collect(Collectors.joining(",")) + ") ";
|
|
|
|
|
+ tenantParams.addAll(tenantIds);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (beginTime != null && !beginTime.isEmpty()) {
|
|
|
|
|
+ tenantSql += "AND t.create_time >= ? ";
|
|
|
|
|
+ tenantParams.add(beginTime + " 00:00:00");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (endTime != null && !endTime.isEmpty()) {
|
|
|
|
|
+ tenantSql += "AND t.create_time <= ? ";
|
|
|
|
|
+ tenantParams.add(endTime + " 23:59:59");
|
|
|
|
|
+ }
|
|
|
|
|
+ tenantSql += "GROUP BY t.tenant_id, COALESCE(t.biz_type, 'OTHER') ORDER BY t.tenant_id";
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> tenantRows = jdbcTemplate.queryForList(tenantSql, tenantParams.toArray());
|
|
|
|
|
+
|
|
|
|
|
+ // 加载租户名称
|
|
|
|
|
+ Set<Long> allTenantIds = tenantRows.stream()
|
|
|
|
|
+ .map(r -> toLong(r.get("tenant_id")))
|
|
|
|
|
+ .filter(Objects::nonNull)
|
|
|
|
|
+ .collect(Collectors.toSet());
|
|
|
|
|
+ Map<Long, String> nameMap = new HashMap<>();
|
|
|
|
|
+ if (!allTenantIds.isEmpty()) {
|
|
|
|
|
+ List<Company> companies = companyMapper.selectCompanyByIds(new ArrayList<>(allTenantIds));
|
|
|
|
|
+ if (companies != null) {
|
|
|
|
|
+ for (Company c : companies) {
|
|
|
|
|
+ nameMap.put(c.getCompanyId(), c.getCompanyName());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 按租户ID分组
|
|
|
|
|
+ Map<Long, ModuleConsumptionVo.TenantItem> tenantMap = new LinkedHashMap<>();
|
|
|
|
|
+ for (Map<String, Object> row : tenantRows) {
|
|
|
|
|
+ Long tid = toLong(row.get("tenant_id"));
|
|
|
|
|
+ String bizType = (String) row.get("biz_type");
|
|
|
|
|
+ BigDecimal amount = toBigDecimal(row.get("total_amount"));
|
|
|
|
|
+
|
|
|
|
|
+ ModuleConsumptionVo.TenantItem ti = tenantMap.computeIfAbsent(tid, k -> {
|
|
|
|
|
+ ModuleConsumptionVo.TenantItem t = new ModuleConsumptionVo.TenantItem();
|
|
|
|
|
+ t.setTenantId(k);
|
|
|
|
|
+ t.setTenantName(nameMap.getOrDefault(k, "未知(" + k + ")"));
|
|
|
|
|
+ return t;
|
|
|
|
|
+ });
|
|
|
|
|
+ ti.getModules().put(bizType, amount);
|
|
|
|
|
+ ti.setTotalAmount(ti.getTotalAmount().add(amount));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 按总消费金额降序排列
|
|
|
|
|
+ List<ModuleConsumptionVo.TenantItem> tenantBreakdown = new ArrayList<>(tenantMap.values());
|
|
|
|
|
+ tenantBreakdown.sort((a, b) -> b.getTotalAmount().compareTo(a.getTotalAmount()));
|
|
|
|
|
+ vo.setTenantBreakdown(tenantBreakdown);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 汇总
|
|
|
|
|
+ ModuleConsumptionVo.Summary summary = new ModuleConsumptionVo.Summary();
|
|
|
|
|
+ summary.setTotalAmount(totalAmount);
|
|
|
|
|
+ summary.setTotalCount(totalCount);
|
|
|
|
|
+ summary.setTenantCount((long) tenantMap.size());
|
|
|
|
|
+ summary.setModuleCount(moduleBreakdown.size());
|
|
|
|
|
+ vo.setSummary(summary);
|
|
|
|
|
+
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private ModuleConsumptionVo buildEmptyReport() {
|
|
|
|
|
+ ModuleConsumptionVo vo = new ModuleConsumptionVo();
|
|
|
|
|
+ vo.setSummary(new ModuleConsumptionVo.Summary());
|
|
|
|
|
+ vo.setModuleBreakdown(new ArrayList<>());
|
|
|
|
|
+ vo.setTenantBreakdown(new ArrayList<>());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private BigDecimal toBigDecimal(Object v) {
|
|
|
|
|
+ if (v == null) return BigDecimal.ZERO;
|
|
|
|
|
+ if (v instanceof BigDecimal) return (BigDecimal) v;
|
|
|
|
|
+ return new BigDecimal(String.valueOf(v));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Long toLong(Object v) {
|
|
|
|
|
+ if (v == null) return null;
|
|
|
|
|
+ if (v instanceof Number) return ((Number) v).longValue();
|
|
|
|
|
+ return Long.parseLong(String.valueOf(v));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|