瀏覽代碼

chore: 迁移项目plan文件到 .claude/plan/

alphah 1 天之前
父節點
當前提交
8959e273df

+ 36 - 0
.claude/plan/admin-id-id-id-golden-lamport.md

@@ -0,0 +1,36 @@
+# Plan: 首页筛选框合并到同一行
+
+## Context
+
+商户下拉框单独一个 `<div>` 放在标题下方,与 `.section-header-right` 里的收款方类型+企业下拉框视觉分离太远。将它们合并到同一个 `.section-header-right` 中。
+
+## 修改
+
+**`frontend/src/views/dashboard/tenant.vue`** — 把商户下拉框移入 `.section-header-right`,删掉独立的外层 div:
+
+```html
+<!-- 现有 L28-56: section-header-right 只有 payee + enterprise -->
+<div class="section-header-right">
+  <el-select v-model="selectedPayeeType" ... />
+  <el-select v-model="selectedEnterpriseId" ... />
+</div>
+
+<!-- L59-68: 商户在单独 div -->
+<div v-if="is_platform_user" style="margin-bottom: 16px; width: 300px;">
+  <el-select v-model="currentTenantId" ... />
+</div>
+
+<!-- 改为: 三个都在 section-header-right 里 -->
+<div class="section-header-right">
+  <el-select v-if="is_platform_user" v-model="currentTenantId" placeholder="选择商户" style="width: 180px">
+    <el-option v-for="tenant in allTenantData" :key="tenant.id" :label="tenant.name" :value="tenant.id" />
+  </el-select>
+  <el-select v-model="selectedPayeeType" ... />
+  <el-select v-model="selectedEnterpriseId" ... />
+</div>
+```
+
+## 验证
+
+- admin 首页 → 商户、收款方类型、企业三个下拉框在同一行紧邻排列
+- 普通用户 → 只有收款方类型、企业两个下拉框(商户不显示,无回归)

+ 33 - 0
.claude/plan/calm-scribbling-marble.md

@@ -0,0 +1,33 @@
+# DTO 回填 — 最后 5 个模块
+
+## Context
+
+前次会话已补齐 34/35 项。最后一项:给已有 Service 的 5 个模块补类型化 DTO,消除 `Map<String,Object>` 内部数据传递。
+
+## 目标模块
+
+| 模块 | 现有文件 | 需要创建 |
+|------|---------|---------|
+| `system/menu` | MenuService, MenuController, MenuEntity | MenuCreateDTO, MenuUpdateDTO, MenuVO, MenuQueryDTO |
+| `payment/enterprise` | EnterpriseService, EnterpriseController, EnterpriseEntity | EnterpriseCreateDTO, EnterpriseUpdateDTO, EnterpriseVO, EnterpriseQueryDTO |
+| `payment/account` | AccountService, AlipayTransferService, AccountController | AccountCreateDTO, TransferCreateDTO, DepositVO, WithdrawVO 等 |
+| `system/role` | RoleService, RoleController, RoleEntity | RoleCreateDTO, RoleUpdateDTO, RoleVO, RoleQueryDTO |
+| `system/user` | UserService, UserController, UserEntity | UserCreateDTO, UserUpdateDTO, UserVO, UserQueryDTO |
+
+## 模板参照
+
+- `module/system/dict/dto/DictTypeCreateDTO.java` — Create DTO 模板
+- `module/system/dict/dto/DictTypeVO.java` — VO 模板
+- `module/system/dict/service/DictService.java` — Service 使用 DTO 的参考
+
+## 执行方式
+
+5 个模块无依赖关系,用子代理并行处理。每个模块需要:
+1. 读 Entity 字段
+2. 创建 3-4 个 DTO/VO 文件
+3. 修改 Service 使用 DTO 替代 `Map<String,Object>`
+4. 修改 Controller 使用类型化 DTO
+
+## 验证
+
+`mvn compile` 通过

+ 155 - 0
.claude/plan/melodic-marinating-eich.md

@@ -0,0 +1,155 @@
+# 当面付代开通功能实现计划
+
+## Context
+
+需要在 xjz-payment-platform 中新增"代商家开通当面付"功能。这是支付宝 ISV 服务商代商家开通当面付收单产品的标准流程,涉及 4 个 API 的顺序调用:
+
+1. `alipay.open.agent.create` → 创建事务,获得 `batch_no`
+2. `alipay.open.agent.facetoface.sign` → 提交当面付开通申请
+3. `alipay.open.agent.confirm` → 确认提交事务
+4. `alipay.open.agent.order.query` → 轮询申请单状态
+
+SDK 验证:所有 Request/Response 类在 `alipay-sdk-python-all` 中可用。`FacetofaceSign` 无独立 Model 类,直接在 Request 上设置属性。
+
+## 实现方案
+
+### 1. 后端模块 `backend/app/plugin/module_payment/facetoface/`
+
+完全复用现有 module_payment 的 model/crud/service/controller/schema 模式。
+
+#### `enums.py` - 申请单状态枚举
+
+```
+INIT            - 初始化(本地状态,尚未提交)
+SUBMITTED       - 已提交到支付宝
+MERCHANT_AUDITING - 审核中
+MERCHANT_CONFIRM  - 审核通过,等待商家确认
+SUCCESS         - 商家已确认,开通成功
+CLOSED          - 已关闭(驳回/失败/取消)
+```
+
+#### `model.py` - FacetofaceOrderModel
+
+表名 `pay_facetoface_order`,继承 `PaymentModelMixin + TenantMixin`。
+
+关键字段:
+- `batch_no` (String64, unique) - 支付宝事务编号
+- `order_no` (String64) - 支付宝申请单号
+- `order_status` (String32) - 申请单状态(用枚举)
+- `merchant_name` (String128) - 商户名称
+- `shop_name` (String128) - 店铺名称
+- `shop_address` (String256) - 店铺地址
+- `mcc_code` (String32) - 商户类别码
+- `rate` (String16) - 费率
+- `business_license_no` (String64) - 营业执照号
+- `business_license_mobile` (String32) - 联系手机号
+- `sign_and_auth` (Boolean) - 是否同时获取授权
+- `confirm_url` (Text) - 商家确认链接
+- `app_auth_token` (String128) - 商家授权 token
+- `reject_reason` (Text) - 驳回原因
+- `remark` (Text) - 备注
+- `last_query_time` (DateTime) - 最后查询时间
+- `next_query_time` (DateTime) - 下次查询时间
+- `query_count` (Integer) - 已查询次数
+
+#### `schema.py`
+
+- `FacetofaceApplySchema` - 申请请求(shop_name, shop_address, mcc_code, rate, business_license_no, business_license_mobile, sign_and_auth 等)
+- `FacetofaceOrderOutSchema` - 详情响应
+- `FacetofaceOrderListOutSchema` - 列表响应
+
+#### `crud.py`
+
+- `FacetofaceCRUD` 继承 `CRUDBase`
+- `get_by_batch_no` - 按事务编号查询
+- `get_pending_orders` - 获取待轮询的申请单
+
+#### `service.py`
+
+- `apply_service` - 执行三步操作:create → sign → confirm,一次性完成提交
+- `query_order_service` - 手动查询单个申请单状态
+- `poll_pending_orders` - 定时任务调用,轮询所有待处理申请单
+  - 策略:提交后 5 分钟首查,之后每 4 小时查一次
+  - 如果 MERCHANT_AUDITING 超过 1 天,标记需关注
+
+#### `controller.py`
+
+- `FacetofaceRouter = APIRouter(prefix="/facetoface", tags=["当面付开通"])`
+- `POST /facetoface/apply` - 提交开通申请
+- `GET /facetoface` - 查询申请单列表(分页)
+- `POST /facetoface/{id}/query` - 手动刷新查询状态
+- `GET /facetoface/{id}` - 查询申请单详情
+
+#### `__init__.py`
+
+导出 Router, CRUD, Model, Service 等。
+
+### 2. 模块注册
+
+`backend/app/plugin/module_payment/__init__.py` 的 `_MODULES` 列表加入 `"facetoface"`。
+
+路由自动发现:discover.py 会自动扫描 `module_payment/facetoface/controller.py` 并注册到 `/payment/facetoface`。
+
+### 3. 数据库迁移
+
+创建 Alembic migration:`backend/app/alembic/versions/c1d2e3f4g5h6_add_facetoface_order.py`
+
+### 4. 定时轮询任务
+
+在应用启动时注册一个 interval 定时任务(通过现有的 APScheduler 系统),每 30 分钟检查一次是否有需要查询状态的申请单。具体轮询间隔由 `next_query_time` 字段控制:
+- 提交后 5 分钟 → 首查
+- 之后每 4 小时查一次
+- 达到终态(SUCCESS/CLOSED)后停止轮询
+
+### 5. 前端
+
+#### `frontend/src/api/module_payment/facetoface.ts`
+
+API 接口定义 + 状态常量映射。
+
+#### `frontend/src/views/module_payment/facetoface/index.vue`
+
+管理页面,包含:
+- 搜索栏:商户名称、店铺名称、状态筛选
+- 数据表格:batch_no、商户名、店铺名、状态、费率、确认链接、创建时间
+- 新增按钮:打开申请表单弹窗
+- 操作列:查询状态、查看详情
+- 申请表单弹窗:填写商户信息后提交
+
+复用现有 CRUD 组件:PageSearch, PageContent, CrudToolbarLeft, CrudToolbarRight, PageModal。
+
+### 6. 关键文件清单
+
+**新建:**
+- `backend/app/plugin/module_payment/facetoface/__init__.py`
+- `backend/app/plugin/module_payment/facetoface/enums.py`
+- `backend/app/plugin/module_payment/facetoface/model.py`
+- `backend/app/plugin/module_payment/facetoface/schema.py`
+- `backend/app/plugin/module_payment/facetoface/crud.py`
+- `backend/app/plugin/module_payment/facetoface/service.py`
+- `backend/app/plugin/module_payment/facetoface/controller.py`
+- `backend/app/alembic/versions/c1d2e3f4g5h6_add_facetoface_order.py`
+- `frontend/src/api/module_payment/facetoface.ts`
+- `frontend/src/views/module_payment/facetoface/index.vue`
+
+**修改:**
+- `backend/app/plugin/module_payment/__init__.py` - 注册模块
+
+### 7. 复用的现有代码
+
+- `AlipayClient.get_client()` from `app.core.alipay.client`
+- `PaymentModelMixin, TenantMixin` from `app.core.base_model`
+- `CRUDBase` from `app.core.base_crud`
+- `CustomException` from `app.core.exceptions`
+- `AuthPermission, redis_getter` from `app.core.dependencies`
+- `OperationLogRoute` from `app.core.router_class`
+- `ResponseSchema, SuccessResponse` from `app.common.response`
+- `scheduler` from `app.core.ap_scheduler` (注册轮询任务)
+- 前端: PageSearch, PageContent, CrudToolbarLeft/Right, PageModal 组件
+
+### 8. 验证
+
+1. 后端:启动服务,检查 `/payment/facetoface` 路由是否注册
+2. 数据库:运行 migration 创建表
+3. 前端:访问当面付管理页面,测试列表、搜索、新增申请
+4. 定时任务:检查 APScheduler 日志确认轮询任务注册成功

+ 61 - 0
.claude/plan/read-c-users-1-claude-claude-md-before-s-breezy-rivest.md

@@ -0,0 +1,61 @@
+# Plan: 商户端看板 — 商户筛选下拉框
+
+## Context
+
+商户看板需要支持筛选:全部 / 本商户 / 各子商户,默认"全部"(本商户+子商户),今日数据和近30天数据两张卡片同步联动。
+
+## 实现策略
+
+现有查询通过 `merchant_id IN (SELECT id FROM merchant WHERE id = #{tenantId} OR parent_merchant_id = #{tenantId})` 实现租户级聚合。在此之上叠加一个 `scopeId` 参数:当非空时追加 `AND merchant_id = #{scopeId}`,精确过滤到指定商户。
+
+- `scopeId = null` → 全部(当前行为不变)
+- `scopeId = loggedInMerchantId` → 仅本商户自己的交易
+- `scopeId = subMerchantId` → 仅该子商户
+
+## 改动清单
+
+### 1. MerchantMapper — 新增子商户查询
+- `selectSubMerchants(@Param("merchantId") Long merchantId)` 返回 `List<Map<String,Object>>`
+- XML: `SELECT id, merchant_name AS merchantName FROM merchant WHERE parent_merchant_id = #{merchantId} AND status = 1 ORDER BY id`
+
+### 2. TransactionMapper — 6 个方法增加 scopeId 参数
+- `getTenantTodayStats` — 加 `@Param("scopeId") Long scopeId`
+- `getTenantTodayRefundStats` — 同上
+- `getTenantTodayDisputeCount` — 同上
+- `getTransactionTrend` — 同上
+- `getPaymentMethodDistribution` — 同上
+- `getStatusDistribution` — 同上
+
+### 3. TransactionMapper.xml — 6 个查询增加 scopeId 条件
+每个查询在 tenantId 条件之后追加:
+```xml
+<if test="scopeId != null">AND merchant_id = #{scopeId}</if>
+```
+
+### 4. MerchantDashboardController — 3 处变更
+- 新增 `@GetMapping("/merchants")` 返回子商户列表供下拉框
+- `/today` 端点:`merchantId` → `resolvedId`(根据 scopeId 决定),所有 mapper 调用传入 `scopeId`
+- `/transaction-trend`、`/payment-method-distribution`、`/status-distribution` 各加 `@RequestParam(required = false) Long scopeId`
+
+### 5. 前端 dashboard.js — API 全部加 scopeId 参数
+- `getSubMerchants()` 新增
+- `getTransactionTrend(params)` — 已有 params,传 scopeId
+- `getPaymentMethodDistribution(params)` — 加 params
+- `getStatusDistribution(params)` — 加 params
+- `getTodayStats(params)` — 加 params
+
+### 6. DashboardView.vue — 顶部加下拉框
+- `el-select` 三个 option:全部、本商户、各子商户
+- selectedMerchantId 响应式变量,默认 null(全部)
+- `watch` 监听变化,重新 fetch 全部数据
+- 今日和近30天卡片都使用同一个 selectedMerchantId
+
+## 不涉及
+- admin 看板
+- 非看板相关的 merchant 接口
+
+## 验证
+1. 下拉默认"全部",数据与改造前一致
+2. 选"本商户",两个卡片数据更新为仅该商户
+3. 选子商户,两个卡片数据更新为仅该子商户
+4. 下拉切换后两个区域同步刷新

+ 340 - 0
.claude/plan/read-c-users-1-claude-claude-md-before-s-fluttering-pancake.md

@@ -0,0 +1,340 @@
+# Java vs Python 差异修复计划
+
+## 上下文
+
+基于前一轮审查报告,以 Python (FastAPI) 为参考标准,修复 Java (Spring Boot) 后端中发现的全部 14 项差异(P0/P1/P2)。所有修改仅涉及 Java 端代码(除 P2-12 需同步修复 Python float 精度问题)。
+
+---
+
+## P0: 立即修复(5项)
+
+### P0-1: 通知幂等性 — 添加 Redis 去重
+
+**文件**: `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\notification\service\NotificationService.java`
+
+**改动**:
+1. 注入 `RedisLockUtil`(已有 `RedisTemplate` 可用)
+2. 在 `verifyAndDispatch()` 方法开头(L67 提取 notify_id 后),用 `"notify:" + notifyId` 作为 key 调 `redisLockUtil.lock(key, 60)`
+3. 若 lock 返回 null → 重复通知 → 直接 return `"success"`(不 return `"fail"`,以免触发支付宝重试)
+4. 在 finally 块中调 `redisLockUtil.unlock(key, lockValue)` 释放
+
+**验证**: 用相同 notify_id 连续两次调用 POST /alipay,第二次应直接返回 success 而不触发 handler
+
+---
+
+### P0-2: POST /alipay 响应格式 — 改为纯文本
+
+**文件**: `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\notification\controller\NotificationController.java`
+
+**改动**:
+```java
+// 改前:
+@PostMapping("/alipay")
+public Result<String> alipayNotify(@RequestParam Map<String, String> params) {
+    String result = notificationService.verifyAndDispatch(params);
+    return Result.ok(result);
+}
+
+// 改后:
+@PostMapping("/alipay")
+public ResponseEntity<String> alipayNotify(@RequestParam Map<String, String> params) {
+    String result = notificationService.verifyAndDispatch(params);
+    return ResponseEntity.ok()
+            .contentType(MediaType.TEXT_PLAIN)
+            .body(result);
+}
+```
+删除 `Result.ok()` 包装,改为 `ResponseEntity<String>` + `text/plain` Content-Type
+
+**验证**: curl POST /alipay,响应体应为纯文本 `success` 或 `fail`,非 JSON
+
+---
+
+### P0-3: statConsumeAmount — 实现真实查询
+
+**文件**: 
+- `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\account\service\AccountService.java`
+- `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\notification\mapper\PayBillMapper.java`
+
+**改动**:
+1. 在 `AccountService` 注入 `PayBillMapper`
+2. 在 `PayBillMapper` 添加自定义查询方法(参考 `doStatAmount` 的 CASE-WHEN 聚合模式):
+```java
+@Select("""
+    SELECT
+      COALESCE(SUM(CASE WHEN gmt_recieve_pay >= CURRENT_DATE
+        THEN consume_amount ELSE 0 END), 0) as amount_of_today,
+      COALESCE(SUM(CASE WHEN gmt_recieve_pay >= CURRENT_DATE - INTERVAL '7 days'
+        THEN consume_amount ELSE 0 END), 0) as amount_of_7days,
+      COALESCE(SUM(consume_amount), 0) as amount_of_all
+    FROM pay_bill WHERE consume_type = 'CONSUME' AND status = 'PROCESSED'
+    AND tenant_id = #{tenantId}
+    AND enterprise_id = #{enterpriseId}
+    AND employee_id = #{payeeType}
+    """)
+Map<String, BigDecimal> statConsumeAmount(@Param("tenantId") Long tenantId,
+    @Param("enterpriseId") String enterpriseId,
+    @Param("payeeType") String payeeType);
+```
+3. 替换 `AccountService.statConsumeAmount()` 的硬编码桩为调用此方法
+
+**验证**: 插入测试数据到 pay_bill,调用 GET /stat/consume/amount,应返回非零值
+
+---
+
+### P0-4: 重试锁 — AtomicBoolean → Redis 分布式锁
+
+**文件**: `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\account\service\AlipayTransferService.java`
+
+**改动**:
+1. 注入 `RedisLockUtil`
+2. 在 `retryDealingTransfers()` 中,将 L458 的 `retryRunning.compareAndSet(false, true)` 替换为:
+```java
+String lockValue = redisLockUtil.lock("retry:dealing_transfers", 120);
+if (lockValue == null) {
+    log.debug("[重试任务] 上一轮尚未完成或另一实例执行中,跳过");
+    return;
+}
+```
+3. 将 L550 的 `retryRunning.set(false)` 替换为:
+```java
+redisLockUtil.unlock("retry:dealing_transfers", lockValue);
+```
+4. 删除 L448 的 `AtomicBoolean retryRunning` 字段
+
+**验证**: 确认多实例部署时不重复执行;进程崩溃后 TTL 120s 到期自动释放
+
+---
+
+### P0-5: JWT 密钥 — 统一密钥为 Python 版本
+
+**文件**: `D:\project2\payment-platform\java\src\main\resources\application.yml`
+
+**当前值** (L52): `vgb0tnl9d58+6n-6h-ea&u^1#0sccp!794=krylxcjq75vzps$`
+**Python值**: `vgb0tnl9d58+6n-6h-ea&u^1#1sccp!794=krylxcjq75vzps$`
+**差异**: 第24字符 `#0` vs `#1`
+
+**改动**: 改为 `vgb0tnl9d58+6n-6h-ea&u^1#1sccp!794=krylxcjq75vzps$`(Python 版本)
+
+**验证**: 用 Python 生成的 token 调 Java 端点,应能通过认证
+
+---
+
+## P1: 尽快修复(4项)
+
+### P1-1: IssueBatchService — 补充缺失方法
+
+**文件**: 
+- `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\expense\quota\service\IssueBatchService.java`
+- Python参考: `D:\project2\payment-platform\backend\app\plugin\module_payment\expense\quota\service.py` (L657-740)
+
+**现有状态**: `create()`, `cancel()`, `list()` 已实现。`records()` 被错误映射到 `page()`
+
+**改动**:
+1. 添加 `records(String batchNo, int page, int size)` 方法 — 按 batch_no 查询 `QuotaEntity` 中属于该批次的条目(参考 Python `issue_batch_records_query_service`)
+2. 修改 `QuotaController.java` L148-153 的 `POST /payment/quota/issuebatch/records` 从调用 `issueBatchService.page()` 改为调用 `issueBatchService.records()`
+
+**验证**: 创建批次后调 POST /issuebatch/records,应返回该批次的员工额度明细而非批次头列表
+
+---
+
+### P1-2: AuthService — 补全验证码 & SMS 验证
+
+**文件**: `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\system\auth\service\AuthService.java`
+
+**现状**: `CaptchaService`、`SmsCodeService`、`AutoLoginService` 三个服务类已存在但未在 AuthService 中调用
+
+**改动**:
+1. 注入 `CaptchaService`
+2. 在 `login()` 方法开头添加验证码验证:
+```java
+if (request.getCaptchaKey() != null) {
+    if (!captchaService.verify(request.getCaptchaKey(), request.getCaptchaCode())) {
+        throw new BusinessException(ErrorCode.CAPTCHA_ERROR);
+    }
+}
+```
+3. 在 `LoginRequest` DTO 添加 `captchaKey` / `captchaCode` 字段(如缺失)
+4. 在 `loginSms()` 开头添加 SMS验证:
+```java
+if (!smsCodeService.verifyCode(request.getPhone(), request.getSmsCode())) {
+    throw new BusinessException(ErrorCode.SMS_CODE_ERROR);
+}
+```
+5. 在 `loginSms()` 添加员工激活状态检查(参考 Python L208-214)
+
+**验证**: 
+- 登录时传错误验证码 → 返回验证码错误
+- SMS登录传错误code → 返回 SMS 验证失败
+- 未激活员工 SMS 登录 → 返回激活状态错误
+
+---
+
+### P1-3: PointsController — 租户范围对齐
+
+**文件**: 
+- `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\points\controller\PointsController.java`
+- `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\points\service\PointsService.java`
+
+**问题**: 当前通过请求参数 `enterprise_id` 显式传入租户,而不是从 auth context 推断 → 存在跨租户越权风险
+
+**改动**:
+1. 在 `PointsService` 或 Controller 中从 `SecurityContextHolder` 获取当前 `LoginUser`
+2. `GET /payment/points` — 自动过滤 `tenant_id = currentUser.getTenantId()`
+3. `POST /payment/points/{id}/add` / `deduct` — 从 auth 获取 enterprise_id,而非 DTO
+4. 添加 `GET /payment/points` 直接返回当前租户积分(而非分页列表),与 Python `GET  /payment/points` 语义对齐
+5. 保留 `/{id}` 路径的 CRUD 作为管理端点(管理员需要,Python 没有但 Java 多出的 CRUD 保留)
+
+**验证**: 
+- A租户用户调 GET /points → 仅返回A租户积分
+- A租户用户调 POST /points/{B的id}/add → 返回403
+
+---
+
+### P1-4: BillHandler — 修复字段映射错误
+
+**文件**: `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\notification\handler\BillHandler.java`
+
+**Bug 1** (L266): `orderItem.getOrderType()` 被赋值给 `orderStatus`,应改为 `orderItem.getOrderStatus()`
+**Bug 2** (L323): `voucher.getVoucherContent()` 被赋值给 `voucherStatus`,应改为 `voucher.getVoucherStatus()`
+
+**改动**:
+```java
+// L266 改前:  orderEntity.setOrderStatus(orderItem.getOrderType());
+// L266 改后:  orderEntity.setOrderStatus(orderItem.getOrderStatus());
+
+// L323 改前:  voucher.setVoucherStatus(v.getVoucherContent());
+// L323 改后:  voucher.setVoucherStatus(v.getVoucherStatus());
+```
+
+**验证**: 支付宝通知到达后,检查 DB 中 order.order_status 和 voucher.voucher_status 是否为实际状态值
+
+---
+
+## P2: 重构优化(5项)
+
+### P2-1: PayDepartmentEntity — 继承基类
+
+**文件**: `D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\department\entity\PayDepartmentEntity.java`
+
+**改动**:
+1. 将 `public class PayDepartmentEntity implements Serializable` 改为 `public class PayDepartmentEntity extends PaymentTenantBaseEntity`
+2. 删除与基类重复的字段声明: `id`, `tenantId`, `status`, `description`, `createdTime`, `updatedTime`
+3. 保留 PayDepartmentEntity 特有的字段: `departmentId`, `departmentName`, `departmentCode`, `parentDepartmentId`, `sortOrder`, `leaderEmployeeId`, `leaderEmployeeName`, `enterpriseId`
+4. 如果 ID 需要 AUTO 而非 ASSIGN_ID,在 `id` 上添加 `@TableId(type = IdType.AUTO)` 覆盖基类默认值
+
+**验证**: 编译通过 + 部门 CRUD 功能正常
+
+---
+
+### P2-2: JSON 字段 — 添加 @JsonRawValue
+
+**受影响文件** (6个entity):
+- `TransferEntity.java` — `payeeInfo`, `extInfo`
+- `PayBillEntity.java` — `notifyMsg`, `extInfos`
+- `EmployeeEntity.java` — `departmentIds`, `accountingEntityIds`, `labelNames`, `profiles`, `departmentList`, `roleList`
+- `EnterpriseEntity.java` — `baseInfo`, `profiles`
+- `AlipayNotifyLogEntity.java` — `message`
+- `DepositEntity.java` / `WithdrawEntity.java` — `notifyContent`
+
+**改动**: 对每个 JSON 字段的 getter 添加 `@JsonRawValue`,setter 添加 `@JsonRawValue`
+```java
+@JsonRawValue
+public String getPayeeInfo() { return payeeInfo; }
+
+@JsonRawValue
+public void setPayeeInfo(String payeeInfo) { this.payeeInfo = payeeInfo; }
+```
+
+这确保 Jackson 序列化时不会对已经 JSON 编码的字符串进行二次转义
+
+**验证**: 序列化 TransferEntity 到 JSON,payeeInfo 字段应为展开的 JSON 对象而非转义字符串
+
+---
+
+### P2-3: Python float → Decimal
+
+**文件**: 
+- `D:\project2\payment-platform\backend\app\plugin\module_payment\expense\institution\model.py`
+- `D:\project2\payment-platform\backend\app\plugin\module_payment\expense\rule\model.py`
+
+**改动**:
+```python
+from decimal import Decimal
+
+# institution/model.py L62-64
+amount: Mapped[Decimal | None] = mapped_column(
+    Numeric(12, 2), default=0, comment="发放金额")
+
+# institution/model.py L65-67
+single_limit: Mapped[Decimal | None] = mapped_column(
+    Numeric(12, 2), default=0, comment="单次限额")
+
+# rule/model.py L43-45
+single_limit: Mapped[Decimal | None] = mapped_column(
+    Numeric(12, 2), default=0, comment="单次限额")
+```
+
+**验证**: Python 测试金额运算,确认精度无损失
+
+---
+
+### P2-4: AlipayConfig — 多租户支持
+
+**文件**: 
+- `D:\project2\payment-platform\java\src\main\java\com\payment\platform\core\alipay\AlipayClientFactory.java`
+- `D:\project2\payment-platform\java\src\main\java\com\payment\platform\core\alipay\AlipayConfig.java`
+
+**改动**:
+1. `AlipayClientFactory` 中:
+   - 将 `private AlipayClient client` 单例改为 `private final Map<String, AlipayClient> clients = new ConcurrentHashMap<>()`
+   - 新增 `getClient(String enterpriseId)` 方法: 从 DB `open_conf` 表获取该企业的 Alipay 配置,懒加载创建 client
+   - 保留 `getClient()` (无参) 作为默认/兜底 client
+2. 各调用方 (`AlipayTransferService`, `AlipayDepartmentService` 等) 改为调 `getClient(enterpriseId)` 代替 `getClient()`
+
+**验证**: 两家企业用不同 app_id 调用 Alipay API,确认各自使用正确凭证
+
+---
+
+### P2-5: DataScopeInnerInterceptor — 补全数据权限
+
+**文件**: `D:\project2\payment-platform\java\src\main\java\com\payment\platform\core\permission\DataScopeInnerInterceptor.java`
+
+**当前状态**: 仅实现 scope=1(仅本人),仅过滤 4 张系统表
+
+**改动**:
+1. 注入 `DeptService` 用于查询部门子树
+2. 扩展 `DATA_SCOPE_TABLES` 列表包含 `pay_department`, `pay_employee`, `pay_transfer`, `pay_bill` 等支付表
+3. 从 `LoginUser` 读取 `dataScope` 字段(需确认 LoginUser 是否已有此字段,如无则添加)
+4. 按 data_scope 值分支:
+   - **1 仅本人**: `WHERE created_id = userId`(已实现)
+   - **2 本部门**: `WHERE dept_id = userDeptId`
+   - **3 本部门及以下**: `WHERE dept_id IN (userDeptId + 递归子部门ID列表)`
+   - **4 全部**: 不添加任何过滤(已实现 — 跳过 interceptor)
+   - **5 自定义**: 不添加过滤(业务层处理)
+5. 将 L94 的 `log.warn("数据权限SQL改写失败, 使用原始SQL", e)` 在生产环境改为抛出异常(fail-closed 而非 fail-open)
+
+**验证**: 不同 data_scope 角色的用户查询同一列表,返回结果集应不同
+
+---
+
+## 实施顺序
+
+```
+第一轮 (并行可做): P0-1, P0-2, P0-3, P0-5 (独立文件)
+第二轮: P0-4 (依赖 RedisLockUtil 确认可用)
+第三轮: P1-1, P1-2, P1-4 (独立文件)
+第四轮: P1-3 (依赖 SecurityContext 模式确认)
+第五轮: P2-1, P2-2, P2-3 (纯重构,不改变行为)
+第六轮: P2-4, P2-5 (架构变更,需额外测试)
+```
+
+## 验证方法
+
+1. **编译**: `mvn compile -f java/pom.xml` 确保无编译错误
+2. **单元测试**: 每个改动提交前运行对应模块测试
+3. **集成验证**:
+   - P0-2: `curl -X POST localhost:8081/api/v1/payment/notify/alipay -d '...'` → 响应为纯文本
+   - P0-5: 用 Python 签发 token → 调 Java 端点 → 200 OK
+   - P1-3: 用租户A token → 调 GET /payment/points → 仅返回A的数据
+4. **Swagger UI**: 打开 `http://localhost:8081/swagger-ui.html` 确认端点路径和响应结构正确

+ 125 - 0
.claude/plan/read-c-users-1-claude-claude-md-before-s-idempotent-harp.md

@@ -0,0 +1,125 @@
+# 邀请码系统 — 前端 + 后端增强
+
+## Context
+
+后端(Java)已实现邀请码的生成/校验/列表/删除。SQL 已执行。现在需要:
+1. 注册页面添加邀请码输入框(模板已有注释掉的代码,启用即可)
+2. 超管后台新增邀请码管理页面(生成 + 列表 + 删除)
+3. 后端 list 接口需增强,当前只返回 code 字符串,前端需要完整数据(用户名、时间等)
+4. 数据库添加菜单条目
+
+---
+
+## 一、后端增强
+
+### 1.1 新增 InvitationCodeVO
+
+**新建**: `java/src/main/java/com/payment/platform/module/system/invite/dto/InvitationCodeVO.java`
+
+```java
+// 字段:id, code, status, description, createdBy(创建者用户名), createdTime,
+//        usedBy(使用者用户名), usedTime
+```
+
+### 1.2 修改 InvitationCodeService.list()
+
+**修改**: `InvitationCodeService.java`
+
+- 返回类型从 `PageResult<String>` 改为 `PageResult<InvitationCodeVO>`
+- 查 user 表填充 createdBy → 用户名、usedBy → 用户名
+- 注入 UserMapper 做关联查询
+
+### 1.3 修改 InvitationCodeController.list()
+
+**修改**: `InvitationCodeController.java`
+
+- 返回类型适配新的 VO
+
+---
+
+## 二、前端 API 层
+
+### 2.1 新建邀请码 API
+
+**新建**: `frontend/src/api/module_system/invitation.ts`
+
+参照 `params.ts` 模式:
+- `generateInvitation(body: { count: number; description?: string })` → POST `/system/invite/generate`
+- `listInvitation(query: InvitationPageQuery)` → GET `/system/invite/list`
+- `deleteInvitation(body: number[])` → DELETE `/system/invite/delete`
+
+### 2.2 修改用户 API
+
+**修改**: `frontend/src/api/module_system/user.ts`
+
+- `RegisterForm` 接口取消注释 `invite_code: string`(第 153 行)
+
+---
+
+## 三、前端页面
+
+### 3.1 注册页 — 启用邀请码字段
+
+**修改**: `frontend/src/views/module_system/auth/components/Register.vue`
+
+模板和校验规则中已有注释掉的邀请码代码,取消注释即可:
+- 模板第 5-12 行:邀请码输入框(el-form-item prop="invite_code")
+- 脚本第 142-148 行:invite_code 校验规则
+
+### 3.2 邀请码管理页
+
+**新建**: `frontend/src/views/module_system/invitation/index.vue`
+
+参照 `notice/index.vue` 的 CURD 模式但更简单(无编辑/详情):
+- **搜索区**: code(文本)+ status(下拉:未使用/已使用)
+- **工具栏**: 生成邀请码按钮 + 删除按钮 + 刷新/列过滤
+- **表格列**: code(邀请码), status(状态标签 0=绿色"未使用"/1=红色"已使用"), description(备注), created_by(创建者), created_time(创建时间), used_by(使用者), used_time(使用时间), 操作(删除)
+- **生成弹窗**: EnhancedDialog 内含表单 — count(数量,默认1)+ description(备注,可选)
+- **删除**: 确认对话框,支持批量
+
+组件使用:
+```
+PageSearch → PageContent(CrudToolbarLeft + CrudToolbarRight + el-table) → EnhancedDialog(生成弹窗)
+```
+
+权限前缀:`module_system:invitation`
+
+---
+
+## 四、菜单 SQL
+
+### 4.1 新建菜单迁移
+
+**新建**: `java/sql/005_invitation_menu.sql`
+
+参照 `003_service_provider_menu.sql` 模式:
+```sql
+-- 在「系统管理」目录下新增「邀请码管理」菜单
+-- 权限标识: module_system:invitation:list / generate / delete
+-- 路由: /invitation → views/module_system/invitation/index.vue
+```
+
+---
+
+## 五、文件变更清单
+
+| 操作 | 文件 |
+|------|------|
+| **新建** | `java/.../invite/dto/InvitationCodeVO.java` |
+| **修改** | `java/.../invite/service/InvitationCodeService.java` |
+| **修改** | `java/.../invite/controller/InvitationCodeController.java` |
+| **新建** | `frontend/src/api/module_system/invitation.ts` |
+| **修改** | `frontend/src/api/module_system/user.ts` |
+| **修改** | `frontend/src/views/module_system/auth/components/Register.vue` |
+| **新建** | `frontend/src/views/module_system/invitation/index.vue` |
+| **新建** | `java/sql/005_invitation_menu.sql` |
+
+## 六、验证
+
+1. 后端编译:`mvn compile -f java/pom.xml`
+2. 执行 `005_invitation_menu.sql`
+3. 前端启动:`cd frontend && pnpm dev`
+4. 测试:
+   - 超管登录 → 左侧菜单出现「邀请码管理」→ 生成邀请码 → 列表显示
+   - 注册页 → 邀请码输入框可见 → 必填校验 → 输入有效码注册成功 → 管理页显示"已使用"
+   - 管理页 → 删除未使用的邀请码 → 确认删除成功

+ 141 - 0
.claude/plan/read-c-users-1-claude-claude-md-before-s-misty-hummingbird.md

@@ -0,0 +1,141 @@
+# Plan: Alipay SDK Integration into Java Expense Services
+
+## Context
+
+Python backend had FULL Alipay SDK integration for ~18 expense-control APIs. Java port has `alipay-sdk-java` v4.39.218 in pom.xml + `AlipayClientFactory` bean producing `DefaultAlipayClient` — but 5 expense module services return empty maps with `log.warn("NOT_IMPL")` where they should call the SDK.
+
+**5 payment modules already use real SDK calls** as the pattern reference:
+- `AlipayEmployeeService` (D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\employee\service\AlipayEmployeeService.java)
+- `AlipayTransferService`, `AlipayEnterpriseService`, `AlipayDepartmentService`, `FacetofaceService`
+
+Goal: Replace 18 NOT_IMPL stubs in expense module with real `alipayClientFactory.getClient().execute(request)` calls.
+
+## Canonical Java SDK Pattern
+
+From `AlipayEmployeeService.java` (lines 42-70):
+```java
+// 1. Build model
+AlipayXxxModel model = new AlipayXxxModel();
+model.setField(value);
+// 2. Build request
+AlipayXxxRequest request = new AlipayXxxRequest();
+request.setBizModel(model);
+// 3. Execute
+AlipayXxxResponse response = alipayClientFactory.getClient().execute(request);
+// 4. Check
+if (!response.isSuccess())
+    throw new BusinessException(400, "失败: " +
+        (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));
+// 5. Catch
+catch (AlipayApiException e) {
+    throw new BusinessException(400, "支付宝异常: " + e.getMessage());
+}
+```
+
+Use wildcard imports: `import com.alipay.api.domain.*;` / `request.*;` / `response.*;` / `AlipayApiException`
+
+## Implementation (4 phases, 5 files)
+
+### Phase 1: InstitutionService (8 APIs)
+File: `java/src/main/java/com/payment/platform/module/payment/expense/institution/service/InstitutionService.java`
+Python: `backend/app/plugin/module_payment/expense/institution/service.py`
+Already has `AlipayClientFactory` injected.
+
+| API | Alipay SDK Class | Notes |
+|-----|-----------------|-------|
+| `create()` | `AlipayEbppInvoiceInstitutionCreateModel/Request/Response` | Replace UUID fallback with `response.getInstitutionId()` |
+| `update()` | `AlipayEbppInvoiceInstitutionModifyModel/Request/Response` | Map DTO fields to model |
+| `delete()` | `AlipayEbppInvoiceInstitutionDeleteModel/Request/Response` | Best-effort: swallow Alipay errors, always clean up local DB |
+| `detail()` | `AlipayEbppInvoiceInstitutionDetailinfoQueryModel/Request/Response` | Try Alipay first, fall back to DB |
+| `listScope()` | `AlipayEbppInvoiceInstitutionScopepageinfoQueryModel/Request/Response` | Note SDK typo: `getOnwerOpenIdList()` |
+| `modifyScope()` | `AlipayEbppInvoiceInstitutionScopeModifyModel/Request/Response` | Python `delete_owner_id_list` → Java `setRemoveOwnerIdList` |
+| `createIssueRule()` | `AlipayEbppInvoiceIssueruleCreateModel/Request/Response` | Includes issue_target_info_list |
+| `updateIssueRule()` / `deleteIssueRule()` | Modify/Delete variants | |
+
+### Phase 2: InstitutionScopeSyncService (1 API)
+File: `java/src/main/java/com/payment/platform/module/payment/expense/institution/service/InstitutionScopeSyncService.java`
+Already has `AlipayClientFactory`. Replace `scope.modify` stubs for department/employee removal. Errors logged, don't block caller.
+
+### Phase 3: QuotaService (3 stubs)
+File: `java/src/main/java/com/payment/platform/module/payment/expense/quota/service/QuotaService.java`
+Python: `backend/app/plugin/module_payment/expense/quota/service.py`
+Already has `AlipayClientFactory` (adjustInternal + outsourceNotify already work).
+
+| Method | Alipay SDK Class |
+|--------|-----------------|
+| `create(QuotaCreateDTO)` | `AlipayEbppInvoiceExpensecontrolQuotaCreateModel/Request/Response` |
+| `expenseCreate(Map)` | Same quota.create API, different DTO packaging |
+| `expenseQuery(Map)` | `AlipayEbppInvoiceExpensecontrolQuotaQueryModel/Request/Response` |
+
+### Phase 4: RuleService + IssueBatchService (5 APIs)
+**Need `AlipayClientFactory` injection added** — both currently lack it.
+
+**RuleService**: `java/.../expense/rule/service/RuleService.java`
+Python: `backend/app/plugin/module_payment/expense/rule/service.py`
+
+| Method | Alipay SDK Class |
+|--------|-----------------|
+| `createExpense(Map)` | `AlipayEbppInvoiceInstitutionExpenseruleCreateModel/Request/Response` |
+| `modifyExpense(String, Map)` | `AlipayEbppInvoiceInstitutionExpenseruleModifyModel/Request/Response` |
+| `deleteExpense(String, Map)` | `AlipayEbppInvoiceInstitutionExpenseruleDeleteModel/Request/Response` |
+
+**IssueBatchService**: `java/.../expense/quota/service/IssueBatchService.java`
+
+| Method | Alipay SDK Class |
+|--------|-----------------|
+| `create(Map)` | `AlipayEbppInvoiceExpensecontrolIssuebatchCreateModel/Request/Response` |
+| `cancel(String)` | `AlipayEbppInvoiceExpensecontrolIssuebatchCancelModel/Request/Response` |
+
+### Phase 5: QuotaService expenseModify + expenseDelete (2 remaining stubs)
+
+File: `java/.../expense/quota/service/QuotaService.java`
+Python: `backend/app/plugin/module_payment/expense/quota/service.py`
+Already has `AlipayClientFactory` injected.
+
+| Method | Alipay SDK Class | Notes |
+|--------|-----------------|-------|
+| `expenseModify(String, Map)` | `AlipayEbppInvoiceExpensecontrolQuotaModifyModel/Request/Response` | action=ADD/DEDUCT/MODIFY_SHARE_MODE, need quota_id from local DB via out_biz_no |
+| `expenseDelete(String)` | `AlipayCommerceEcExpenseDeleteModel/Request/Response` | Simple delete by out_biz_no |
+
+### Phase 6: Python↔Java 全量字段对齐修复
+
+Files: IssueBatchService, RuleService, QuotaService, InstitutionService
+
+**CRITICAL (wrong data / SDK rejection):**
+| File | Method | Fix |
+|------|--------|-----|
+| IssueBatchService.create | `target_id`→`owner_id`, `target_type`→`owner_type`, `quota_amount`→`issue_quota` (3 wrong JSON keys) |
+| IssueBatchService.create | Add `effective_start_date` / `effective_end_date` to model (Python required) |
+| RuleService.modifyExpense | Add `institution_id` on model (Python required) |
+| QuotaService.expenseModify | `quota_id` from `data.get("quota_id")`, `outer_source_id` from `data.get("outer_source_id")` — NOT from local DB / parameter |
+
+**SEVERE (logic mismatch):**
+| File | Method | Fix |
+|------|--------|-----|
+| InstitutionService.createFullFlow | Rollback: delete Alipay institution if scope/issuerule fails (Python re-raises) |
+| InstitutionService.createFullFlow | Map `standard_id_info_list` from Alipay response instead of using input data |
+| RuleService.modifyExpense | `action` from `data.get("action")` not hardcoded |
+| IssueBatchService.create | Add `issue_desc`, `owner_open_id`, `user_name` optional fields |
+
+**MEDIUM (optional fields):**
+| File | Method | Fix |
+|------|--------|-----|
+| RuleService.createExpense | Add `payment_policy`, `consume_mode`, `open_rule_id`, `rule_name` conditionals |
+| RuleService.modifyExpense | Add `standard_desc`, `open_rule_id`, `payment_policy`, `consume_mode` conditionals |
+| InstitutionService.createFullFlow | Add `currency` to institution create model |
+| QuotaService.expenseDelete | Raise BusinessException when not found (match Python), keep SDK gap note |
+| QuotaService.expenseModify | Add null response guard, add error log before throw, handle share_mode empty string |
+
+### Error handling rules
+
+- Normal APIs: Throw `BusinessException` on `!response.isSuccess()`
+- Institution delete: Swallow Alipay errors (Python best-effort pattern)
+- Institution detail: Alipay failure → DB fallback
+- Scope sync: Log + continue, don't block
+
+## Verification
+
+1. `mvn compile` — all SDK classes from `alipay-sdk-java-4.39.218.ALL.jar` compile
+2. Field mapping: each Python `data["field_name"]` → Java `model.setFieldName(...)` verified against Python reference
+3. SDK naming quirks handled: `removeOwnerIdList` (not delete), `getOnwerOpenIdList()` (typo)
+4. All 5 services have `AlipayClientFactory` injection