5 Коміти 16fc5a4ee0 ... 216bbd1819

Автор SHA1 Опис Дата
  alphaH 216bbd1819 fix bug 2 тижнів тому
  alphaH 5e83aa3dbc feat: 首页新增当面付收款统计卡片,汇总统计纳入当面付金额 2 тижнів тому
  alphaH dfe5023e18 fix: gmt_payment 修正 Alipay SDK UTC 偏移 + gmtCreate 显式 Asia/Shanghai + VO 补 updatedTime 2 тижнів тому
  alphaH 41e8daf14d fix: 前端 prop 改为 snake_case 匹配 Jackson SNAKE_CASE 序列化 2 тижнів тому
  alphaH d4f8e2d4c5 fix: 收款记录组件 admin 不预设 enterprise 过滤,与消费记录/转账记录行为一致 2 тижнів тому

+ 10 - 0
frontend/src/api/module_payment/account.ts

@@ -33,6 +33,16 @@ export const AccountAPI = {
     });
   },
 
+  statF2fTradeAmount(tenantId?: number, enterpriseId?: string) {
+    return request<
+      ApiResponse<{ amount_of_today: string; amount_of_7days: string; amount_of_all: string }>
+    >({
+      url: `${API_PATH}/stat/f2f/amount`,
+      method: "get",
+      params: { tenant_id: tenantId, enterprise_id: enterpriseId },
+    });
+  },
+
   authorizeApply(data: { enterprise_id: string }) {
     return request<ApiResponse<{ sign_url: string }>>({
       url: `${API_PATH}/authorize/apply`,

+ 38 - 1
frontend/src/views/dashboard/tenant.vue

@@ -102,6 +102,24 @@
         </div>
       </div>
 
+      <div class="section-header" style="margin-top: 24px;">
+        <h2>当面付收款统计</h2>
+      </div>
+      <div class="data-cards">
+        <div class="data-card f2f-card">
+          <div class="card-title">今日收款金额(元)</div>
+          <div class="card-value">{{ f2fAmount.amount_of_today || "--" }}</div>
+        </div>
+        <div class="data-card f2f-card">
+          <div class="card-title">近7日收款金额(元)</div>
+          <div class="card-value">{{ f2fAmount.amount_of_7days || "--" }}</div>
+        </div>
+        <div class="data-card f2f-card">
+          <div class="card-title">总收款金额(元)</div>
+          <div class="card-value">{{ f2fAmount.amount_of_all || "--" }}</div>
+        </div>
+      </div>
+
       <div class="section-header" style="margin-top: 24px;">
         <h2>汇总统计</h2>
       </div>
@@ -124,7 +142,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
+import { ref, computed, onMounted } from 'vue';
 import { ElButton, ElTabs, ElTabPane } from 'element-plus';
 import { useEnterpriseStore, useUserStore } from '@/store';
 import AccountAPI from '@/api/module_payment/account';
@@ -146,6 +164,7 @@ const selectedEnterpriseId = ref<string | undefined>(
 const selectedPayeeType = ref<string | undefined>(undefined);
 function handlePayeeTypeChange() {
   fetchStatAmount();
+  fetchF2fAmount();
   fetchSummaryAmount();
 }
 
@@ -166,6 +185,12 @@ const consumeAmount = ref<{
   amount_of_all?: string;
 }>({});
 
+const f2fAmount = ref<{
+  amount_of_today?: string;
+  amount_of_7days?: string;
+  amount_of_all?: string;
+}>({});
+
 const summaryAmount = ref<{
   amount_of_today?: string;
   amount_of_7days?: string;
@@ -196,6 +221,15 @@ async function fetchConsumeAmount() {
   }
 }
 
+async function fetchF2fAmount() {
+  try {
+    const res = await AccountAPI.statF2fTradeAmount(currentTenantId.value, selectedEnterpriseId.value);
+    f2fAmount.value = res.data.data || {};
+  } catch (error) {
+    console.error("获取当面付收款统计失败:", error);
+  }
+}
+
 async function fetchSummaryAmount() {
   try {
     const res = await AccountAPI.statSummaryAmount(currentTenantId.value, selectedEnterpriseId.value, selectedPayeeType.value);
@@ -229,6 +263,7 @@ onMounted(async () => {
   }
   fetchStatAmount();
   fetchConsumeAmount();
+  fetchF2fAmount();
   fetchSummaryAmount();
 });
 
@@ -241,12 +276,14 @@ watch(currentTenantId, async (newTenantId) => {
   selectedEnterpriseId.value = undefined;
   fetchStatAmount();
   fetchConsumeAmount();
+  fetchF2fAmount();
   fetchSummaryAmount();
 });
 
 watch(selectedEnterpriseId, () => {
   fetchStatAmount();
   fetchConsumeAmount();
+  fetchF2fAmount();
   fetchSummaryAmount();
 });
 

+ 20 - 16
frontend/src/views/module_payment/account/components/F2fTradeRecord.vue

@@ -45,34 +45,34 @@
         <template #empty>
           <el-empty description="暂无数据" />
         </template>
-        <el-table-column prop="outTradeNo" label="商户订单号" min-width="200" />
-        <el-table-column prop="tradeNo" label="支付宝交易号" min-width="200">
+        <el-table-column prop="out_trade_no" label="商户订单号" min-width="200" />
+        <el-table-column prop="trade_no" label="支付宝交易号" min-width="200">
           <template #default="{ row }">
-            {{ row.tradeNo || '-' }}
+            {{ row.trade_no || '-' }}
           </template>
         </el-table-column>
-        <el-table-column prop="totalAmount" label="收款金额" min-width="120">
-          <template #default="{ row }">¥{{ row.totalAmount }}</template>
+        <el-table-column prop="total_amount" label="收款金额" min-width="120">
+          <template #default="{ row }">¥{{ row.total_amount }}</template>
         </el-table-column>
         <el-table-column prop="subject" label="收款标题" min-width="150" />
-        <el-table-column prop="tradeStatus" label="交易状态" min-width="100">
+        <el-table-column prop="trade_status" label="交易状态" min-width="100">
           <template #default="{ row }">
-            <el-tag :type="TRADE_STATUS_TAG_TYPE[row.tradeStatus] || 'info'">
-              {{ TRADE_STATUS_LABEL[row.tradeStatus] || row.tradeStatus }}
+            <el-tag :type="TRADE_STATUS_TAG_TYPE[row.trade_status] || 'info'">
+              {{ TRADE_STATUS_LABEL[row.trade_status] || row.trade_status }}
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="buyerLogonId" label="付款方" min-width="180">
+        <el-table-column prop="buyer_logon_id" label="付款方" min-width="180">
           <template #default="{ row }">
-            {{ row.buyerLogonId || '-' }}
+            {{ row.buyer_logon_id || '-' }}
           </template>
         </el-table-column>
-        <el-table-column prop="gmtPayment" label="付款时间" min-width="160">
+        <el-table-column prop="gmt_payment" label="付款时间" min-width="160">
           <template #default="{ row }">
-            {{ row.gmtPayment || '-' }}
+            {{ row.gmt_payment || '-' }}
           </template>
         </el-table-column>
-        <el-table-column prop="createdTime" label="创建时间" min-width="160" />
+        <el-table-column prop="created_time" label="创建时间" min-width="160" />
       </el-table>
       <div class="mt-4 flex justify-end">
         <el-pagination
@@ -95,10 +95,12 @@ import facetofaceApi, {
   TRADE_STATUS_LABEL,
   TRADE_STATUS_OPTIONS,
 } from "@/api/module_payment/facetoface";
-import { useEnterpriseStore } from "@/store";
-import { ref, reactive, onMounted } from "vue";
+import { useEnterpriseStore, useUserStore } from "@/store";
+import { ref, reactive, computed, onMounted } from "vue";
 
 const enterpriseStore = useEnterpriseStore();
+const userStore = useUserStore();
+const isPlatformUser = computed(() => userStore.is_platform_user);
 
 const loading = ref(false);
 const list = ref<any[]>([]);
@@ -150,7 +152,9 @@ function handleSearchReset() {
 
 onMounted(async () => {
   await enterpriseStore.loadEnterpriseList();
-  searchForm.enterprise_id = enterpriseStore.getCurrentEnterprise?.enterprise_id;
+  if (!isPlatformUser.value) {
+    searchForm.enterprise_id = enterpriseStore.getCurrentEnterprise?.enterprise_id;
+  }
   fetchList();
 });
 </script>

+ 8 - 0
java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java

@@ -46,6 +46,14 @@ public class AccountController {
         return Result.ok(accountService.statConsumeAmount(tenantId, enterpriseId, payeeType));
     }
 
+    @PreAuthorize("@perm.hasAny('module_payment:account:transfer')")
+    @GetMapping("/stat/f2f/amount")
+    public Result<Map<String, String>> statF2fTradeAmount(
+            @RequestParam(name = "tenant_id", required = false) Long tenantId,
+            @RequestParam(name = "enterprise_id", required = false) String enterpriseId) {
+        return Result.ok(accountService.statF2fTradeAmount(tenantId, enterpriseId));
+    }
+
     @PreAuthorize("@perm.hasAny('module_payment:account:transfer')")
     @GetMapping("/stat/summary/amount")
     public Result<Map<String, String>> statSummaryAmount(

+ 47 - 4
java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java

@@ -18,6 +18,8 @@ import com.payment.platform.module.payment.account.mapper.TransferMapper;
 import com.payment.platform.module.payment.account.mapper.WithdrawMapper;
 import com.payment.platform.module.payment.notification.entity.PayBillEntity;
 import com.payment.platform.module.payment.notification.mapper.PayBillMapper;
+import com.payment.platform.module.payment.facetoface.entity.F2fTradeRecordEntity;
+import com.payment.platform.module.payment.facetoface.mapper.F2fTradeRecordMapper;
 import com.payment.platform.common.exception.BusinessException;
 import com.payment.platform.core.alipay.AlipayClientFactory;
 import com.alipay.api.AlipayApiException;
@@ -63,6 +65,7 @@ public class AccountService {
     private final AlipayClientFactory alipayClientFactory;
     private final RedisTemplate<String, Object> redisTemplate;
     private final PayBillMapper payBillMapper;
+    private final F2fTradeRecordMapper f2fTradeRecordMapper;
     private final AlipayTransferService alipayTransferService;
 
     public List<AccountVO> queryAccounts(String enterpriseId) {
@@ -90,16 +93,56 @@ public class AccountService {
     }
 
     /**
-     * 汇总统计金额 — 转账 + 消费
+     * 统计当面付收款金额 — 仅统计 TRADE_SUCCESS 状态的记录
+     */
+    public Map<String, String> statF2fTradeAmount(Long tenantId, String enterpriseId) {
+        OffsetDateTime today = LocalDate.now().atStartOfDay(ZoneOffset.ofHours(8)).toOffsetDateTime();
+        OffsetDateTime weekAgo = today.minusDays(7);
+
+        LambdaQueryWrapper<F2fTradeRecordEntity> base = new LambdaQueryWrapper<>();
+        base.eq(F2fTradeRecordEntity::getTradeStatus, "TRADE_SUCCESS");
+        if (tenantId != null) base.eq(F2fTradeRecordEntity::getTenantId, tenantId);
+        if (enterpriseId != null && !enterpriseId.isBlank())
+            base.eq(F2fTradeRecordEntity::getEnterpriseId, enterpriseId);
+
+        // 分三次查询简单清晰,当面付收款量不会太大
+        BigDecimal todayAmount = sumF2fTradeAmount(
+                base.clone().ge(F2fTradeRecordEntity::getGmtPayment, today));
+        BigDecimal weekAmount = sumF2fTradeAmount(
+                base.clone().ge(F2fTradeRecordEntity::getGmtPayment, weekAgo));
+        BigDecimal allAmount = sumF2fTradeAmount(base);
+
+        return Map.of(
+                "amount_of_today", todayAmount.toPlainString(),
+                "amount_of_7days", weekAmount.toPlainString(),
+                "amount_of_all", allAmount.toPlainString());
+    }
+
+    private BigDecimal sumF2fTradeAmount(LambdaQueryWrapper<F2fTradeRecordEntity> w) {
+        List<F2fTradeRecordEntity> records = f2fTradeRecordMapper.selectList(w);
+        return records.stream()
+                .map(r -> {
+                    try { return new BigDecimal(r.getTotalAmount()); }
+                    catch (Exception e) { return BigDecimal.ZERO; }
+                })
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+
+    /**
+     * 汇总统计金额 — 转账 + 消费 + 当面付收款
      */
     public Map<String, String> statSummaryAmount(Long tenantId, String enterpriseId, String payeeType) {
         Map<String, String> transfer = statTransferAmount(tenantId, enterpriseId, payeeType);
         Map<String, String> consume = statConsumeAmount(tenantId, enterpriseId, payeeType);
+        Map<String, String> f2f = statF2fTradeAmount(tenantId, enterpriseId);
 
         return Map.of(
-                "amount_of_today", add(transfer.get("amount_of_today"), consume.get("amount_of_today")),
-                "amount_of_7days", add(transfer.get("amount_of_7days"), consume.get("amount_of_7days")),
-                "amount_of_all", add(transfer.get("amount_of_all"), consume.get("amount_of_all")));
+                "amount_of_today", add(add(transfer.get("amount_of_today"), consume.get("amount_of_today")),
+                        f2f.get("amount_of_today")),
+                "amount_of_7days", add(add(transfer.get("amount_of_7days"), consume.get("amount_of_7days")),
+                        f2f.get("amount_of_7days")),
+                "amount_of_all", add(add(transfer.get("amount_of_all"), consume.get("amount_of_all")),
+                        f2f.get("amount_of_all")));
     }
 
     private String add(String a, String b) {

+ 4 - 0
java/src/main/java/com/payment/platform/module/payment/expense/institution/service/InstitutionScopeSyncService.java

@@ -229,6 +229,8 @@ public class InstitutionScopeSyncService {
                 quota.setInstitutionId(inst.getInstitutionId());
                 quota.setOutBizNo("all_" + inst.getInstitutionId() + "_" + employeeId);
                 quota.setEnterpriseId(enterpriseId);
+                // 通知回调无认证上下文,需显式设置 tenantId(TenantLineInnerInterceptor 会跳过)
+                quota.setTenantId(inst.getTenantId());
 
                 if (isActive) {
                     BigDecimal amount = inst.getAmount() != null ? inst.getAmount() : BigDecimal.ZERO;
@@ -383,6 +385,8 @@ public class InstitutionScopeSyncService {
                         quota.setInstitutionId(instId);
                         quota.setOutBizNo("scope_" + instId + "_" + employeeId);
                         quota.setEnterpriseId(enterpriseId);
+                        // 通知回调无认证上下文,需显式设置 tenantId(TenantLineInnerInterceptor 会跳过)
+                        quota.setTenantId(inst.getTenantId());
                         if (isActive) {
                             BigDecimal amount = inst.getAmount() != null ? inst.getAmount() : BigDecimal.ZERO;
                             quota.setTotalAmount(amount);

+ 3 - 0
java/src/main/java/com/payment/platform/module/payment/facetoface/dto/F2fTradeRecordVO.java

@@ -46,4 +46,7 @@ public class F2fTradeRecordVO {
 
     @Schema(description = "创建时间")
     private OffsetDateTime createdTime;
+
+    @Schema(description = "更新时间")
+    private OffsetDateTime updatedTime;
 }

+ 7 - 3
java/src/main/java/com/payment/platform/module/payment/facetoface/service/FacetofaceService.java

@@ -415,7 +415,7 @@ public class FacetofaceService {
         tradeRecord.setSubject(StrUtil.isNotBlank(dto.getSubject()) ? dto.getSubject() : "当面付收款");
         tradeRecord.setTradeStatus(F2fTradeStatus.WAIT_BUYER_PAY.getValue());
         tradeRecord.setEnterpriseId(dto.getEnterpriseId());
-        tradeRecord.setGmtCreate(OffsetDateTime.now());
+        tradeRecord.setGmtCreate(OffsetDateTime.now(java.time.ZoneId.of("Asia/Shanghai")));
         tradeRecord.setNextQueryTime(OffsetDateTime.now().plusMinutes(1));
         tradeRecord.setQueryCount(0);
         tradeRecord.setStatus("0");
@@ -514,7 +514,9 @@ public class FacetofaceService {
             record.setBuyerId(response.getBuyerUserId());
             record.setBuyerLogonId(response.getBuyerLogonId());
             if (response.getSendPayDate() != null) {
-                record.setGmtPayment(response.getSendPayDate().toInstant().atOffset(java.time.ZoneOffset.ofHours(8)));
+                // Alipay SDK 将北京时间字符串按 JVM 默认时区(UTC)解析,instant 偏 +8h,修正后转为 +08:00
+                record.setGmtPayment(response.getSendPayDate().toInstant()
+                        .minusSeconds(8 * 3600).atOffset(java.time.ZoneOffset.ofHours(8)));
             }
             // 终态不继续轮询
             if (alipayTradeStatus != null &&
@@ -650,7 +652,9 @@ public class FacetofaceService {
             record.setBuyerId(response.getBuyerUserId());
             record.setBuyerLogonId(response.getBuyerLogonId());
             if (response.getSendPayDate() != null) {
-                record.setGmtPayment(response.getSendPayDate().toInstant().atOffset(java.time.ZoneOffset.ofHours(8)));
+                // Alipay SDK 将北京时间字符串按 JVM 默认时区(UTC)解析,instant 偏 +8h,修正后转为 +08:00
+                record.setGmtPayment(response.getSendPayDate().toInstant()
+                        .minusSeconds(8 * 3600).atOffset(java.time.ZoneOffset.ofHours(8)));
             }
 
             // 终态停止轮询