瀏覽代碼

@
fix: 通知处理 tenant_id/gmt_biz_create 缺失修复 + 费控制度 pay_expense_rule 写入修复

1. BillHandler: gmtBizCreate解析支持 yyyy-MM-dd HH:mm:ss 格式 + 新记录null兜底
通知回调无认证上下文,根据 enterprise_id 反查 tenantId
2. OrderHandler/VoucherHandler: 通知回调 INSERT pay_bill_order/voucher 时补 tenantId
3. InstitutionService.createFullFlow: 默认 standard_info_list 回写 data,确保
pay_expense_rule 本地DB写入
4. docs/ops.md: 前端部署改为本地build→zip上传→服务器docker cp
@

alphaH 4 小時之前
父節點
當前提交
886e58de3d

+ 8 - 5
docs/ops.md

@@ -69,14 +69,17 @@ strings /tmp/AS.class | grep "<关键字>"
 ## 前端部署
 
 ```bash
-# 本地构建
-cd /home/payments/code/payment-platform/frontend
+# === 本地 ===
+cd D:\project2\payment-platform\frontend
 npm run build
+# 打包上传
+zip -r dist.zip dist/
+scp dist.zip root@172.20.205.37:/root/payments/code/payment-platform/frontend/
 
-# 复制到容器
+# === 服务器 ===
+cd /root/payments/code/payment-platform/frontend
+unzip -o dist.zip
 docker cp dist/. frontend:/usr/share/nginx/html/
-
-# 重载 nginx(如果容器内配置了 nginx)
 docker exec frontend nginx -s reload
 ```
 

+ 79 - 46
java/src/main/java/com/payment/platform/module/payment/expense/institution/service/InstitutionService.java

@@ -204,49 +204,34 @@ public class InstitutionService {
      */
     @Transactional
     public InstitutionVO create(InstitutionCreateDTO dto) {
-        // 如果携带适用范围数据,走完整串联流程
-        if (StrUtil.isNotBlank(dto.getApplicableScope()) && !"none".equals(dto.getApplicableScope())) {
-            Map<String, Object> data = new LinkedHashMap<>();
-            data.put("institution_name", dto.getInstitutionName());
-            data.put("institution_desc", dto.getInstitutionDesc());
-            data.put("enterprise_id", dto.getEnterpriseId());
-            data.put("scene_type", dto.getSceneType());
-            data.put("expense_type", dto.getExpenseType());
-            data.put("expense_sub_type", dto.getExpenseSubType());
-            data.put("effective", dto.getEffective());
-            data.put("effective_start_date", dto.getEffectiveStartDate());
-            data.put("effective_end_date", dto.getEffectiveEndDate());
-            data.put("consult_mode", dto.getConsultMode());
-            data.put("multi_employee_share_mode", dto.getMultiEmployeeShareMode());
-            data.put("grant_mode", dto.getGrantMode());
-            data.put("period_type", dto.getPeriodType());
-            data.put("amount", dto.getAmount());
-            data.put("single_limit", dto.getSingleLimit());
-            data.put("effective_time_type", dto.getEffectiveTimeType());
-            data.put("applicable_scope", dto.getApplicableScope());
-            data.put("currency", dto.getCurrency());
-            data.put("scope_owner_id_list", dto.getScopeOwnerIdList());
-            data.put("scope_owner_type", dto.getScopeOwnerType());
-            data.put("standard_info_list", dto.getStandardInfoList());
-            return createFullFlow(data);
-        }
-
-        // 简单模式:仅创建制度记录
-        String institutionId = UUID.randomUUID().toString().replace("-", "");
-        ExpenseInstitutionEntity entity = BeanUtil.copyProperties(dto, ExpenseInstitutionEntity.class);
-        entity.setInstitutionId(institutionId);
-        // expense_type 映射: GENERAL → DEFAULT (对应 Python controller)
-        if ("GENERAL".equals(entity.getExpenseType())) {
-            entity.setExpenseType("DEFAULT");
-        }
-        // 制度状态不允许外部传入,始终由服务端控制
-        entity.setStatus(InstitutionEnums.InstitutionStatus.INSTITUTION_CREATE.getValue());
-        if (StrUtil.isBlank(entity.getCurrency())) {
-            entity.setCurrency("CNY");
-        }
-        institutionMapper.insert(entity);
-        log.info("创建费控制度成功: institutionId={}, name={}", institutionId, dto.getInstitutionName());
-        return BeanUtil.copyProperties(institutionMapper.selectById(entity.getId()), InstitutionVO.class);
+        // 统一走完整串联流程(对应 Python: 始终调 create_institution_full_flow)
+        Map<String, Object> data = new LinkedHashMap<>();
+        data.put("institution_name", dto.getInstitutionName());
+        data.put("institution_desc", dto.getInstitutionDesc());
+        data.put("enterprise_id", dto.getEnterpriseId());
+        data.put("scene_type", dto.getSceneType());
+        data.put("expense_type", dto.getExpenseType());
+        data.put("expense_sub_type", dto.getExpenseSubType());
+        data.put("effective", dto.getEffective());
+        data.put("effective_start_date", dto.getEffectiveStartDate() != null ? dto.getEffectiveStartDate().toString() : null);
+        data.put("effective_end_date", dto.getEffectiveEndDate() != null ? dto.getEffectiveEndDate().toString() : null);
+        data.put("consult_mode", dto.getConsultMode());
+        data.put("multi_employee_share_mode", dto.getMultiEmployeeShareMode());
+        data.put("grant_mode", dto.getGrantMode());
+        data.put("period_type", dto.getPeriodType());
+        data.put("amount", dto.getAmount());
+        data.put("single_limit", dto.getSingleLimit());
+        data.put("effective_time_type", dto.getEffectiveTimeType());
+        data.put("applicable_scope", dto.getApplicableScope());
+        data.put("currency", dto.getCurrency());
+        data.put("scope_owner_id_list", dto.getScopeOwnerIdList());
+        data.put("scope_owner_type", dto.getScopeOwnerType());
+        data.put("standard_info_list", dto.getStandardInfoList() != null ? dto.getStandardInfoList() : List.of());
+        // 对应 Python: name → institution_name
+        if (data.get("institution_name") == null && dto.getInstitutionName() == null) {
+            data.put("institution_name", "默认制度");
+        }
+        return createFullFlow(data);
     }
 
     /**
@@ -319,7 +304,41 @@ public class InstitutionService {
         @SuppressWarnings("unchecked")
         List<Map<String, Object>> standardInfoList =
                 (List<Map<String, Object>>) data.get("standard_info_list");
-        if (standardInfoList != null && !standardInfoList.isEmpty()) {
+        if (standardInfoList == null || standardInfoList.isEmpty()) {
+            // 对应 Python: 前端不传时自动构造默认使用规则
+            String instName = (String) data.getOrDefault("institution_name", data.get("name"));
+            StandardInfo defaultStd = new StandardInfo();
+            defaultStd.setStandardName(instName != null ? instName : "默认规则");
+            defaultStd.setStandardDesc("通用规则");
+            defaultStd.setStandardId("");
+            defaultStd.setOuterSourceId(java.util.UUID.randomUUID().toString().replace("-", ""));
+            defaultStd.setConsumeMode("DEFAULT");
+            defaultStd.setPaymentPolicy("PERSONAL");
+            defaultStd.setPersonalQrcodeMode(0L);
+            // 默认条件
+            java.util.List<StandardConditionInfo> condList = new java.util.ArrayList<>();
+            StandardConditionInfo defaultCond = new StandardConditionInfo();
+            defaultCond.setRuleFactor("QUOTA_TOTAL");
+            defaultCond.setRuleValue(String.valueOf(data.getOrDefault("single_limit", "0")));
+            condList.add(defaultCond);
+            defaultStd.setStandardConditionInfoList(condList);
+            createModel.setStandardInfoList(java.util.List.of(defaultStd));
+
+            // 回写到 data,确保后续本地DB保存步骤能写入 pay_expense_rule
+            Map<String, Object> defaultStdMap = new LinkedHashMap<>();
+            defaultStdMap.put("outer_source_id", defaultStd.getOuterSourceId());
+            defaultStdMap.put("standard_id", defaultStd.getStandardId());
+            defaultStdMap.put("standard_name", defaultStd.getStandardName());
+            defaultStdMap.put("standard_desc", defaultStd.getStandardDesc());
+            defaultStdMap.put("expense_type_sub_category", "DEFAULT");
+            java.util.List<Map<String, Object>> defaultCondMapList = new java.util.ArrayList<>();
+            Map<String, Object> defaultCondMap = new LinkedHashMap<>();
+            defaultCondMap.put("rule_factor", "QUOTA_TOTAL");
+            defaultCondMap.put("rule_value", String.valueOf(data.getOrDefault("single_limit", "0")));
+            defaultCondMapList.add(defaultCondMap);
+            defaultStdMap.put("standard_condition_info_list", defaultCondMapList);
+            data.put("standard_info_list", java.util.List.of(defaultStdMap));
+        } else {
             List<StandardInfo> stdList = new java.util.ArrayList<>();
             for (Map<String, Object> std : standardInfoList) {
                 StandardInfo si = new StandardInfo();
@@ -489,13 +508,17 @@ public class InstitutionService {
 
         // --- 第4步: 保存制度到本地DB ---
         ExpenseInstitutionEntity entity = new ExpenseInstitutionEntity();
+        // 从认证上下文获取 tenantId
+        Long tenantId = getCurrentTenantId();
+        if (tenantId != null) entity.setTenantId(tenantId);
         entity.setInstitutionId(institutionId);
         entity.setInstitutionName(institutionName);
         entity.setInstitutionDesc((String) data.get("institution_desc"));
         entity.setEnterpriseId(enterpriseId);
         entity.setSceneType((String) data.get("scene_type"));
         entity.setExpenseType(expenseType);
-        entity.setExpenseSubType((String) data.get("expense_sub_type"));
+        String expenseSubType = (String) data.get("expense_sub_type");
+        entity.setExpenseSubType(expenseSubType != null ? expenseSubType : "DEFAULT");
         entity.setStatus(InstitutionEnums.InstitutionStatus.INSTITUTION_CREATE.getValue());
         entity.setCurrency((String) data.getOrDefault("currency", "CNY"));
         entity.setEffective((String) data.getOrDefault("effective", "1"));
@@ -566,6 +589,7 @@ public class InstitutionService {
                 rule.setExpenseTypeSubCategory(
                         (String) std.getOrDefault("expense_type_sub_category", "DEFAULT"));
                 rule.setEnterpriseId(enterpriseId);
+                rule.setTenantId(tenantId);
                 rule.setSingleLimit(singleLimitVal);
 
                 // 序列化 condition_info 为 JSON
@@ -1295,7 +1319,7 @@ public class InstitutionService {
     private LambdaQueryWrapper<ExpenseInstitutionEntity> buildQuery(InstitutionQueryDTO q) {
         LambdaQueryWrapper<ExpenseInstitutionEntity> w = new LambdaQueryWrapper<>();
         if (q == null) return w;
-        if (StrUtil.isNotBlank(q.getEnterpriseId())) w.eq(ExpenseInstitutionEntity::getEnterpriseId, q.getEnterpriseId());
+        if (StrUtil.isNotBlank(q.getEnterprise_id())) w.eq(ExpenseInstitutionEntity::getEnterpriseId, q.getEnterprise_id());
         // 支持 name 和 institutionName 两种前端参数 (对应 Python: name or institution_name)
         String instName = StrUtil.isNotBlank(q.getInstitutionName()) ? q.getInstitutionName() : q.getName();
         if (StrUtil.isNotBlank(instName)) w.like(ExpenseInstitutionEntity::getInstitutionName, instName);
@@ -1611,4 +1635,13 @@ public class InstitutionService {
                 .filter(StrUtil::isNotBlank)
                 .collect(Collectors.toSet());
     }
+
+    private Long getCurrentTenantId() {
+        var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
+        if (auth != null && auth.getPrincipal() instanceof com.payment.platform.core.security.LoginUser user) {
+            return user.getTenantId();
+        }
+        return null;
+    }
+
 }

+ 50 - 2
java/src/main/java/com/payment/platform/module/payment/notification/handler/BillHandler.java

@@ -17,6 +17,8 @@ import com.payment.platform.module.payment.expense.entity.QuotaEntity;
 import com.payment.platform.module.payment.expense.mapper.ExpenseRuleMapper;
 import com.payment.platform.module.payment.expense.mapper.QuotaMapper;
 import com.payment.platform.module.payment.expense.quota.enums.QuotaStatus;
+import com.payment.platform.module.payment.enterprise.entity.EnterpriseEntity;
+import com.payment.platform.module.payment.enterprise.mapper.EnterpriseMapper;
 import com.payment.platform.module.payment.notification.entity.PayBillEntity;
 import com.payment.platform.module.payment.notification.entity.PayBillOrderEntity;
 import com.payment.platform.module.payment.notification.entity.PayBillVoucherEntity;
@@ -53,6 +55,7 @@ public class BillHandler extends BaseNotifyHandler {
     private final TransferMapper transferMapper;
     private final QuotaMapper quotaMapper;
     private final ExpenseRuleMapper expenseRuleMapper;
+    private final EnterpriseMapper enterpriseMapper;
     private final AlipayClientFactory alipayClientFactory;
     private final OpenapiService openapiService;
 
@@ -205,7 +208,19 @@ public class BillHandler extends BaseNotifyHandler {
         bill.setEmployeeId(employeeId);
         bill.setConsumeType(consumeType);
         if (StrUtil.isNotBlank(consumeAmount)) bill.setConsumeAmount(toDecimal(consumeAmount));
-        if (StrUtil.isNotBlank(gmtBizCreate)) bill.setGmtBizCreate(toOffsetDateTime(gmtBizCreate));
+        if (StrUtil.isNotBlank(gmtBizCreate)) {
+            OffsetDateTime parsed = toOffsetDateTime(gmtBizCreate);
+            if (parsed != null) {
+                bill.setGmtBizCreate(parsed);
+            } else if (existing == null) {
+                // 通知中 gmt_biz_create 格式无法解析时,新记录使用当前时间兜底
+                bill.setGmtBizCreate(OffsetDateTime.now());
+                log.warn("账单 gmt_biz_create 解析失败,使用当前时间兜底: pay_no={}, raw={}", payNo, gmtBizCreate);
+            }
+        } else if (existing == null) {
+            // 通知中未提供 gmt_biz_create 时,新记录使用当前时间兜底
+            bill.setGmtBizCreate(OffsetDateTime.now());
+        }
         if (StrUtil.isNotBlank(gmtRecievePay)) bill.setGmtRecievePay(toOffsetDateTime(gmtRecievePay));
         if (StrUtil.isNotBlank(peerPayAmount)) bill.setPeerPayAmount(toDecimal(peerPayAmount));
         bill.setNotifyReason(notifyReason);
@@ -216,6 +231,16 @@ public class BillHandler extends BaseNotifyHandler {
         bill.setExpenseType(expenseType);
         bill.setStatus("NEW");
 
+        // 支付宝通知回调无认证上下文,需根据 enterprise_id 查 tenant_id
+        if (existing == null && bill.getTenantId() == null && StrUtil.isNotBlank(enterpriseId)) {
+            EnterpriseEntity ent = enterpriseMapper.selectOne(
+                    new LambdaQueryWrapper<EnterpriseEntity>()
+                            .eq(EnterpriseEntity::getEnterpriseId, enterpriseId));
+            if (ent != null && ent.getTenantId() != null) {
+                bill.setTenantId(ent.getTenantId());
+            }
+        }
+
         if (existing != null) {
             payBillMapper.updateById(bill);
         } else {
@@ -261,6 +286,8 @@ public class BillHandler extends BaseNotifyHandler {
                 orderEntity.setPayNo(payNo);
                 orderEntity.setOrderNo(orderItem.getOrderId());
                 orderEntity.setTradeNo(orderItem.getBizOutNo());
+                orderEntity.setEnterpriseId(enterpriseId);
+                orderEntity.setTenantId(resolveTenantId(enterpriseId));
                 // orderContent 为 JSON 字符串,含 shop/merchant 等详情
                 orderEntity.setOrderTitle(orderItem.getOrderContent());
                 orderEntity.setOrderStatus(orderItem.getOrderType());
@@ -292,6 +319,8 @@ public class BillHandler extends BaseNotifyHandler {
                     subEntity.setOrderNo(sub.getOrderId());
                     subEntity.setTradeNo(sub.getBizOutNo());
                     subEntity.setOrderStatus(sub.getOrderType());
+                    subEntity.setEnterpriseId(enterpriseId);
+                    subEntity.setTenantId(resolveTenantId(enterpriseId));
                     PayBillOrderEntity exist = billOrderMapper.selectOne(
                             new LambdaQueryWrapper<PayBillOrderEntity>()
                                     .eq(PayBillOrderEntity::getOrderNo, sub.getOrderId()));
@@ -317,6 +346,8 @@ public class BillHandler extends BaseNotifyHandler {
                 if (voucher == null) {
                     voucher = new PayBillVoucherEntity();
                     voucher.setVoucherId(v.getVoucherId());
+                    voucher.setEnterpriseId(enterpriseId);
+                    voucher.setTenantId(resolveTenantId(enterpriseId));
                 }
                 voucher.setPayNo(payNo);
                 voucher.setVoucherType(v.getVoucherType());
@@ -400,6 +431,17 @@ public class BillHandler extends BaseNotifyHandler {
 
     // ---- util ----
 
+    /**
+     * 根据 enterprise_id 反查 tenant_id(通知回调无认证上下文时使用)
+     */
+    private Long resolveTenantId(String enterpriseId) {
+        if (StrUtil.isBlank(enterpriseId)) return null;
+        EnterpriseEntity ent = enterpriseMapper.selectOne(
+                new LambdaQueryWrapper<EnterpriseEntity>()
+                        .eq(EnterpriseEntity::getEnterpriseId, enterpriseId));
+        return ent != null ? ent.getTenantId() : null;
+    }
+
     private static BigDecimal toDecimal(String val) {
         if (StrUtil.isBlank(val)) return null;
         try { return new BigDecimal(val); } catch (NumberFormatException e) { return null; }
@@ -415,7 +457,13 @@ public class BillHandler extends BaseNotifyHandler {
                 return java.time.OffsetDateTime.parse(val,
                         java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME);
             } catch (Exception e2) {
-                return null;
+                try {
+                    return java.time.LocalDateTime.parse(val,
+                            java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+                            .atOffset(java.time.ZoneOffset.ofHours(8));
+                } catch (Exception e3) {
+                    return null;
+                }
             }
         }
     }

+ 14 - 0
java/src/main/java/com/payment/platform/module/payment/notification/handler/OrderHandler.java

@@ -2,6 +2,8 @@ package com.payment.platform.module.payment.notification.handler;
 
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.payment.platform.module.payment.enterprise.entity.EnterpriseEntity;
+import com.payment.platform.module.payment.enterprise.mapper.EnterpriseMapper;
 import com.payment.platform.module.payment.notification.entity.PayBillOrderEntity;
 import com.payment.platform.module.payment.notification.mapper.PayBillOrderMapper;
 import lombok.RequiredArgsConstructor;
@@ -25,6 +27,7 @@ import java.util.Map;
 public class OrderHandler extends BaseNotifyHandler {
 
     private final PayBillOrderMapper orderMapper;
+    private final EnterpriseMapper enterpriseMapper;
 
     @Override
     protected String[] acceptedMethods() {
@@ -82,6 +85,9 @@ public class OrderHandler extends BaseNotifyHandler {
         PayBillOrderEntity order = (existing != null) ? existing : new PayBillOrderEntity();
         order.setOrderNo(orderNo);
         order.setEnterpriseId(enterpriseId);
+        if (existing == null && StrUtil.isNotBlank(enterpriseId)) {
+            order.setTenantId(resolveTenantId(enterpriseId));
+        }
         if (StrUtil.isNotBlank(payNo)) order.setPayNo(payNo);
 
         // trade_no 优先,否则用 out_biz_no (对应 Python: data.trade_no or data.out_biz_no)
@@ -126,4 +132,12 @@ public class OrderHandler extends BaseNotifyHandler {
             }
         }
     }
+
+    private Long resolveTenantId(String enterpriseId) {
+        if (StrUtil.isBlank(enterpriseId)) return null;
+        EnterpriseEntity ent = enterpriseMapper.selectOne(
+                new LambdaQueryWrapper<EnterpriseEntity>()
+                        .eq(EnterpriseEntity::getEnterpriseId, enterpriseId));
+        return ent != null ? ent.getTenantId() : null;
+    }
 }

+ 20 - 0
java/src/main/java/com/payment/platform/module/payment/notification/handler/VoucherHandler.java

@@ -8,6 +8,8 @@ import com.alipay.api.request.AlipayCommerceEcConsumeDetailQueryRequest;
 import com.alipay.api.response.AlipayCommerceEcConsumeDetailQueryResponse;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.payment.platform.core.alipay.AlipayClientFactory;
+import com.payment.platform.module.payment.enterprise.entity.EnterpriseEntity;
+import com.payment.platform.module.payment.enterprise.mapper.EnterpriseMapper;
 import com.payment.platform.module.payment.notification.entity.PayBillVoucherEntity;
 import com.payment.platform.module.payment.notification.mapper.PayBillVoucherMapper;
 import lombok.RequiredArgsConstructor;
@@ -37,6 +39,7 @@ import java.util.Map;
 public class VoucherHandler extends BaseNotifyHandler {
 
     private final PayBillVoucherMapper voucherMapper;
+    private final EnterpriseMapper enterpriseMapper;
     private final AlipayClientFactory alipayClientFactory;
 
     @Override
@@ -81,6 +84,11 @@ public class VoucherHandler extends BaseNotifyHandler {
                 entity = new PayBillVoucherEntity();
                 entity.setVoucherId(id);
                 if (StrUtil.isNotBlank(payNo)) entity.setPayNo(payNo);
+                String entId = params.get("enterprise_id");
+                if (StrUtil.isNotBlank(entId)) {
+                    entity.setEnterpriseId(entId);
+                    entity.setTenantId(resolveTenantId(entId));
+                }
             }
 
             if (StrUtil.isNotBlank(payNo)) entity.setPayNo(payNo);
@@ -152,6 +160,10 @@ public class VoucherHandler extends BaseNotifyHandler {
             PayBillVoucherEntity entity = new PayBillVoucherEntity();
             entity.setVoucherId(v.getVoucherId());
             entity.setPayNo(payNo);
+            if (StrUtil.isNotBlank(enterpriseId)) {
+                entity.setEnterpriseId(enterpriseId);
+                entity.setTenantId(resolveTenantId(enterpriseId));
+            }
             entity.setVoucherType(v.getVoucherType());
             entity.setVoucherStatus(v.getVoucherContent());
             if (StrUtil.isNotBlank(v.getVoucherDate())) {
@@ -185,4 +197,12 @@ public class VoucherHandler extends BaseNotifyHandler {
             }
         }
     }
+
+    private Long resolveTenantId(String enterpriseId) {
+        if (StrUtil.isBlank(enterpriseId)) return null;
+        EnterpriseEntity ent = enterpriseMapper.selectOne(
+                new LambdaQueryWrapper<EnterpriseEntity>()
+                        .eq(EnterpriseEntity::getEnterpriseId, enterpriseId));
+        return ent != null ? ent.getTenantId() : null;
+    }
 }