Преглед изворни кода

revert(订单归属): 撤销 created_id 全套 —— 订单列表回到按租户可见(用户裁定「按租户来」)

用户 2026-10-05 反转 6828f378 的裁定:「这个字段要来有何用?租户id不就行了吗」;
「什么跟什么啊,我不是说了按租户来吗?」。订单列表的可见性按**租户**,不按创建人,
故整提交反做,7 个文件对应回退:

- 读:/page 移除 applyCreatorFilter(非超管恒加 created_id = 本人的谓词)
- 写:收银台建单腿移除 created_id 赋值与 requireUserId()
- 列:LsyOrderEntity.createdId、V1.8 建表语句、db/lsy-order-created-id.sql 一并撤销
  ⇒ 部署不再需要先跑 ALTER;若某库已执行过该脚本,残留列无人读写,无害
- 测试:6828f378 新增的 5 条用例随之回退(回到先前的既有契约)

后果:非超管列表恢复「本租户可见」,收银台 / 开放 API / 免登录收款链接各腿建的
订单都在列表里;created_id 恒 NULL 的单(顾客自助、开放 API、全部历史行)不再被
「本人建的」谓词过滤 —— 这正是这轮「订单看不到」的成因。

全量测试 1082/0/0 BUILD SUCCESS(6828f378 时为 1087,差额即回退的 5 条用例)。
alphaH пре 1 недеља
родитељ
комит
faaf07a314

+ 2 - 32
java/src/main/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingCashierController.java

@@ -175,13 +175,11 @@ public class LeshouyingCashierController {
     @PostMapping("/qrcode")
     public Result<LsyCashierOrderVO> createQr(@Valid @RequestBody LsyCashierCreateDTO dto) {
         Long tenantId = requireTenantId();
-        // ★ 归属(created_id)只在**这条腿**能落 —— 三个建单腿里只有收银台有人类登录态。
-        Long creatorId = requireUserId();
 
         // ★ 本地闸在**任何上游调用之前**。没有商户号 ⇒ 404,一行都不落、一次上游都不发。
         LsyMerchantEntity merchant = merchantService.requireByTenant(tenantId);
 
-        LsyOrderEntity order = newOrder(tenantId, creatorId, merchant, dto);
+        LsyOrderEntity order = newOrder(tenantId, merchant, dto);
         orderMapper.insert(order);
         publisher.changed(OrderEventPublisher.CHANNEL_LSY_ORDER, order.getTenantId());
 
@@ -306,22 +304,15 @@ public class LeshouyingCashierController {
      *       <td>同一列,两个端点各自的名字</td></tr>
      *   <tr><td>{@code attach}</td><td>{@code dto.getAttach()}</td><td>不设(null)</td>
      *       <td>收银台没有「回传字段」这个概念</td></tr>
-     *   <tr><td>{@code createdId}</td><td><b>不设(null)</b></td><td>当前登录账号</td>
-     *       <td>列表归属列(用户 2026-10-05 裁定「只显示本人建的」)。开放 API 腿是 api key 主体、
-     *           免登录收银台腿是合成登录态,两条腿**构造上没有登录账号** ⇒ 恒 NULL;
-     *           历史行同样不回填。列由 {@code db/lsy-order-created-id.sql} 建。</td></tr>
      * </table>
      *
      * <p>★ <b>{@code insert} 必须发生在 {@code mchNo}/{@code shopNo} 装配之后</b>:
      * {@code pay_lsy_order.mch_no} 是 NOT NULL 且无默认值。
      */
-    private LsyOrderEntity newOrder(Long tenantId, Long creatorId, LsyMerchantEntity merchant,
-                                    LsyCashierCreateDTO dto) {
+    private LsyOrderEntity newOrder(Long tenantId, LsyMerchantEntity merchant, LsyCashierCreateDTO dto) {
         OffsetDateTime now = OffsetDateTime.now();
         LsyOrderEntity o = new LsyOrderEntity();
         o.setTenantId(tenantId);
-        // ★ 归属 = 当前登录账号(三个建单腿里只有这里有人类登录态;见上方逐字段差异表)
-        o.setCreatedId(creatorId);
 
         o.setOutTradeNo(OTN_PREFIX + IdUtil.getSnowflakeNextIdStr());
         o.setMchNo(merchant.getMchNo());
@@ -476,27 +467,6 @@ public class LeshouyingCashierController {
         return tenantId;
     }
 
-    /**
-     * 取当前主体的**账号 ID**(订单归属 {@code created_id} 的唯一来源);取不到就**显式拒绝**。
-     *
-     * <p>与 {@link #requireTenantId} 同一条纪律,理由再重一档:{@code created_id} 写错的后果
-     * **静默** —— 落成 null 的行从此对任何非超管不可见(列表谓词是「本人建的」),
-     * 却不会有任何报错。
-     *
-     * <p>理论不可达:本端点前置 {@code @PreAuthorize},能进来的只有 JWT 登录态
-     * ({@code UserDetailsServiceImpl:52-53} 从 {@code sys_user.id} 装配 userId)。
-     * 与 {@code requireTenantId} 一样,这是**防御**,不是流程。
-     */
-    private Long requireUserId() {
-        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
-        LoginUser user = (auth != null && auth.getPrincipal() instanceof LoginUser u) ? u : null;
-        Long userId = user == null ? null : user.getUserId();
-        if (userId == null) {
-            throw new BusinessException(403, "当前登录身份没有账号标识,无法发起收款,请重新登录");
-        }
-        return userId;
-    }
-
     /**
      * 请求体里有不认识的字段 ⇒ 400 + 一句人话,**不是** HTTP 500「服务器内部错误」。
      *

+ 0 - 36
java/src/main/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingOrderController.java

@@ -83,8 +83,6 @@ public class LeshouyingOrderController {
         Page<LsyOrderEntity> page = new Page<>(pageNo, pageSize);
         LambdaQueryWrapper<LsyOrderEntity> w = new LambdaQueryWrapper<>();
         applyTenantFilter(w, tenantId);
-        // ★ 2026-10-05:非超管恒定只查本人创建的(超管豁免)—— 判据与理由见 applyCreatorFilter
-        applyCreatorFilter(w);
         if (StrUtil.isNotBlank(status)) {
             w.eq(LsyOrderEntity::getStatus, status);
         }
@@ -179,40 +177,6 @@ public class LeshouyingOrderController {
         w.eq(LsyOrderEntity::getTenantId, tenantId);
     }
 
-    /**
-     * 只显示**本人(登录账号)创建**的订单 —— 非超管恒定加 {@code created_id = 自己}。
-     *
-     * <p>【为什么需要它】{@code pay_lsy_order} 原先没有任何创建人列,租户下几个入口
-     * (开放 API / 免登录收银台 / 收银台)建的单在列表上混在一起 —— 用户 2026-10-05 报的
-     * 「这些订单有很多都不是用户 q 的」就是这个。裁定(用户拍板):列表只显示本人建的。
-     *
-     * <p>【为什么不用平台现成的 data_scope 机制】{@code DataScopeInnerInterceptor} 存在但
-     * **未接入拦截器链**({@code MyBatisPlusConfig} 只注册了租户/分页/乐观锁三个),
-     * 且它的表清单本来也不含 {@code pay_lsy_order}。动那条全局链会波及所有模块 ——
-     * 本处按它的判据形状(scope=1 即 {@code created_id = 当前用户})在本地实现。
-     *
-     * <p>【为什么超管豁免】与 {@link #applyTenantFilter} 用**同一个开关**
-     * ({@code LoginUser#getIsSuperuser}):超管的定位是跨租户运维,锁到「本人建的」会让它
-     * 在生产上连自己没建过的单都看不到 —— 而它本来就能看全租户。
-     *
-     * <p>【为什么取不到主体 / 主体没有 userId 时**拒绝**而不是给空列表】同
-     * {@code LeshouyingCashierController#requireTenantId} 的纪律:{@code eq(列, null)}
-     * 恒不成立,表现是一个「看起来正常的空页面」,比一句 403 难查得多。本处**没有**拦截器兜底,
-     * 取不到「本人」就必须显式拒绝。
-     */
-    private void applyCreatorFilter(LambdaQueryWrapper<LsyOrderEntity> w) {
-        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
-        LoginUser user = (auth != null && auth.getPrincipal() instanceof LoginUser u) ? u : null;
-        if (user != null && Boolean.TRUE.equals(user.getIsSuperuser())) {
-            return;
-        }
-        Long uid = user == null ? null : user.getUserId();
-        if (uid == null) {
-            throw new BusinessException(403, "当前登录身份没有账号标识,无法查看订单列表,请重新登录");
-        }
-        w.eq(LsyOrderEntity::getCreatedId, uid);
-    }
-
     /** 详情含回调审计轨迹,便于对账排查 */
     @PreAuthorize("@perm.hasAny('module_payment:leshouying:order:list')")
     @GetMapping("/{outTradeNo}")

+ 0 - 17
java/src/main/java/com/payment/platform/module/payment/leshouying/entity/LsyOrderEntity.java

@@ -72,23 +72,6 @@ public class LsyOrderEntity extends PaymentTenantBaseEntity {
 
     private OffsetDateTime finishTime;
 
-    /**
-     * 创建人:**本平台登录账号**({@code sys_user.id}),管理端列表的归属列。
-     *
-     * <p>【谁写】只有收银台人工建单腿({@code LeshouyingCashierController.createQr})落库时写入;
-     * 开放 API 那条腿(api key 主体)与免登录收银台那条腿(合成登录态,没有 userId)
-     * **构造上没有登录账号** ⇒ 恒 NULL。历史行同样恒 NULL —— **不回填**(归属无从考证)。
-     *
-     * <p>【谁读】{@code LeshouyingOrderController#page}:非超管的列表恒加
-     * {@code created_id = 当前账号}(用户 2026-10-05 裁定「只显示本人建的」;
-     * 建列脚本见 {@code db/lsy-order-created-id.sql})。超管不加谓词 ⇒ NULL 行只对超管可见。
-     *
-     * <p>【为什么是显式赋值、不是自动填充】本模块实体不继承 {@code UserBaseEntity},
-     * 且 {@code AutoFillMetaObjectHandler} 只填 createdTime/updatedTime/tenantId ——
-     * {@code createdId} 在本仓的既有写法就是显式 set(同 {@code OperationLogInterceptor:55})。
-     */
-    private Long createdId;
-
     /**
      * 本单**是否有退款正在途中** —— 只读标记,**不是库里的列**({@code exist = false})。
      *

+ 0 - 41
java/src/main/resources/db/lsy-order-created-id.sql

@@ -1,41 +0,0 @@
--- 订单归属列:给 pay_lsy_order 加 created_id(创建人 = 本平台登录账号 ID)
---
--- 【为什么需要单独一个文件】
--- V1.8 用的是 `CREATE TABLE IF NOT EXISTS` —— 它的判据是**关系名**,不是定义。
--- 建表语句执行过之后,对 V1.8 里 `pay_lsy_order` 的**任何修改都静默失效**
--- (只发一条 NOTICE,什么都不做)。而 V1.8 已经执行过,所以新增列必须在**这里**重发一次,
--- 否则文件与库分叉(同款先例:lsy-refund-reconcile-cursor.sql 对 pay_lsy_refund 的处理)。
---
--- 【改的是什么 / 为什么要这一列】
--- 用户 2026-10-05 报「这些订单有很多都不是用户 q 的」—— pay_lsy_order 此前**没有任何创建人列**
--- (user_id 是**上游付款人**标识,不是建单人)⇒ 租户下各入口(开放 API / 免登录收银台 / 收银台)
--- 建的单在管理端列表上混在一起,分不出「谁建的」。裁定(用户拍板):列表**只显示本人建的**。
---
--- 【写入方】只有收银台人工建单腿(LeshouyingCashierController.createQr)写入;
--- 开放 API(api key 主体)与免登录收银台(合成登录态,没有 userId)两条腿**构造上没有登录账号**
--- ⇒ 恒 NULL。
---
--- 【不回填】历史行一律保持 NULL,**不做任何猜测式回填**:归属无从考证,猜一个比 NULL 更坏。
--- 后果(有意):非超管看不到这些历史行(列表谓词是 created_id = 本人);超管不受限。
---
--- 【执行顺序:本文件必须在部署新代码**之前**跑完】
--- 新代码的 selectPage / insert / updateById 的列清单里都含 created_id,
--- 列不存在时会抛 PostgreSQL 42703(与 lsy-refund-reconcile-cursor.sql 同一个执行顺序闸)。
---
--- 【安全性】纯加列 + 注释,不改任何已有列、不动任何已有数据;重复执行是空操作。
---
--- 【执行方式】psql -f 本文件,或用任意客户端逐条跑。跑完用文件末尾两句核对。
-
-ALTER TABLE pay_lsy_order
-    ADD COLUMN IF NOT EXISTS created_id BIGINT;
-
-COMMENT ON COLUMN pay_lsy_order.created_id IS '创建人(本平台登录账号ID;仅收银台人工建单腿写入,其余腿恒NULL)';
-
--- ===== 核对(两句都跑一遍)=====
---
--- 1) 列存在且是 bigint —— 期望一行:created_id | bigint
--- SELECT column_name, data_type FROM information_schema.columns
---  WHERE table_name = 'pay_lsy_order' AND column_name = 'created_id';
---
--- 2) 存量行读数(预期:全部 NULL —— 历史归属不猜测)—— 期望一行两列,attributed 为 0
--- SELECT count(*) AS total, count(created_id) AS attributed FROM pay_lsy_order;

+ 0 - 5
java/src/main/resources/db/migration/V1.8__create_pay_lsy_tables.sql

@@ -84,10 +84,6 @@ CREATE TABLE IF NOT EXISTS pay_lsy_order (
 
     finish_time             TIMESTAMP WITH TIME ZONE,
     description             VARCHAR(255),
-    -- 创建人:**本平台登录账号**(sys_user.id)。三个建单腿里只有收银台有人类登录态(由它写入),
-    -- 开放 API / 免登录收银台两条腿构造上没有登录账号 ⇒ 恒 NULL;历史行同样不回填(归属无从考证)。
-    -- 管理端列表对非超管恒筛 created_id = 本人(2026-10-05 用户裁定);建列/核对见 db/lsy-order-created-id.sql。
-    created_id              BIGINT,
     created_time            TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
     updated_time            TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
 );
@@ -143,7 +139,6 @@ COMMENT ON COLUMN pay_lsy_order.notify_count         IS '下游通知次数';
 COMMENT ON COLUMN pay_lsy_order.last_notify_time     IS '上次通知时间';
 COMMENT ON COLUMN pay_lsy_order.finish_time          IS '完成时间(终态)';
 COMMENT ON COLUMN pay_lsy_order.description          IS '备注/描述';
-COMMENT ON COLUMN pay_lsy_order.created_id           IS '创建人(本平台登录账号ID;仅收银台人工建单腿写入,其余腿恒NULL)';
 COMMENT ON COLUMN pay_lsy_order.created_time         IS '创建时间';
 COMMENT ON COLUMN pay_lsy_order.updated_time         IS '更新时间';
 

+ 0 - 24
java/src/test/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingCashierControllerTest.java

@@ -278,30 +278,6 @@ class LeshouyingCashierControllerTest {
         assertNull(o.getNotifyUrl(), "有意不设 —— 见本方法 javadoc");
     }
 
-    /**
-     * 建单行必须带上**当前登录账号**作为创建人({@code created_id})。
-     *
-     * <p>「列表只显示本人建的」(用户 2026-10-05 裁定)全靠这一笔:三个建单腿里只有收银台
-     * 有人类登录态,开放 API / 免登录收银台两条腿构造上没有登录账号(有意恒 NULL)。
-     * 漏掉这笔的后果是**静默**的:这单建完立刻从操作员自己的列表里消失
-     * (非超管的列表恒筛 {@code created_id = 本人}),却不会有任何报错。
-     */
-    @Test
-    void create_stampsTheLoggedInAccount_asCreator() throws Exception {
-        login(TENANT, false);
-        when(merchantService.requireByTenant(TENANT)).thenReturn(merchantRow());
-        when(orderService.createQrOrder(any(), any())).thenReturn(null);
-
-        mockMvc.perform(post(PREFIX + "/qrcode")
-                        .contentType(MediaType.APPLICATION_JSON)
-                        .content("{\"amount\":" + AMOUNT + "}"))
-                .andExpect(jsonPath("$.code").value(0));
-
-        // 期望值用 Long.valueOf:写成原始类型 1L 会让 null 走到 NPE(错误)而不是断言失败(红)
-        assertEquals(Long.valueOf(1L), insertedOrder().getCreatedId(),
-                "创建人必须是当前登录账号(login() 设的 userId=1)—— 不是租户、不是别的任何值");
-    }
-
     @Test
     void createQr_publishesLsyOrderSignal() throws Exception {
         // 夹具与 create_assemblesTheOrder_rowShapeAndServerDecidedFields 逐行一致

+ 0 - 75
java/src/test/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingOrderControllerTest.java

@@ -342,8 +342,6 @@ class LeshouyingOrderControllerTest {
      */
     @Test
     void page_不传租户_不加租户谓词_由拦截器兜底() {
-        // 2026-10-05 起 /page 还要求可识别的主体(创建人闸);无主体的用例在下方归属组
-        login(3L, false);
         when(orderMapper.selectPage(any(), any())).thenReturn(new Page<>(1, 20));
 
         controller.page(1, 20, null, null, null, null);
@@ -392,76 +390,6 @@ class LeshouyingOrderControllerTest {
         verify(orderMapper, never()).selectPage(any(), any());
     }
 
-    // ───────────────── 订单归属:非超管只看本人创建的(2026-10-05) ─────────────────
-    //
-    // 用户报「这些订单有很多都不是用户 q 的」—— pay_lsy_order 此前**没有任何创建人列**
-    // (user_id 是上游付款人,不是建单人)。裁定(用户拍板):列表只显示本人(登录账号)建的。
-    // created_id 由收银台那条腿写入(见 LeshouyingCashierControllerTest),
-    // 开放 API / 免登录收银台两条腿与全部历史行恒 NULL ⇒ 只对超管可见。
-
-    @Test
-    void page_非超管_只查本人创建的订单() {
-        login(3L, false);   // 普通账号(login 统一设 userId=1);租户谓词由拦截器追加,不在这里
-        when(orderMapper.selectPage(any(), any())).thenReturn(new Page<>(1, 20));
-
-        controller.page(1, 20, null, null, null, null);
-
-        AbstractWrapper<?, ?, ?> w = capturedPageQuery();
-        assertTrue(w.getSqlSegment().contains("created_id"),
-                "非超管的列表必须带创建人谓词;实测: " + w.getSqlSegment());
-        assertEquals(List.of(1L), boundValues(w),
-                "绑的必须是**当前登录账号**(login() 设的 userId=1),多一个少一个都不对");
-    }
-
-    /** 负对照:同一个量具读同一类 wrapper —— 超管这一个开关关掉创建人谓词。 */
-    @Test
-    void page_超管_不加创建人谓词() {
-        login(null, true);
-        when(orderMapper.selectPage(any(), any())).thenReturn(new Page<>(1, 20));
-
-        controller.page(1, 20, null, null, null, null);
-
-        AbstractWrapper<?, ?, ?> w = capturedPageQuery();
-        assertFalse(w.getSqlSegment().contains("created_id"),
-                "超管豁免(锁到「本人建」会让跨租户运维连自己没建过的单都看不到);"
-                        + "实测: " + w.getSqlSegment());
-        assertTrue(boundValues(w).isEmpty(),
-                "超管 + 不传租户 ⇒ 一个谓词都不加;实测绑定值: " + boundValues(w));
-    }
-
-    @Test
-    void page_无登录主体_403_而不是一份空列表() {
-        SecurityContextHolder.clearContext();
-
-        BusinessException e = assertThrows(BusinessException.class,
-                () -> controller.page(1, 20, null, null, null, null));
-
-        assertEquals(403, e.getCode(),
-                "取不到主体 ⇒「本人」无从谈起,必须显式拒绝(本处没有拦截器兜底:data_scope 未接入);"
-                        + "给一份空列表则是「看起来正常的空」");
-        verify(orderMapper, never()).selectPage(any(), any());
-    }
-
-    @Test
-    void page_主体没有账号标识_403() {
-        // 有主体、没 userId(如免登录收银台的合成登录态)—— 同样无法确定「本人」是谁
-        LoginUser u = new LoginUser();
-        u.setUserId(null);
-        u.setUsername("synthetic");
-        u.setStatus("0");
-        u.setTenantId(3L);
-        u.setIsSuperuser(false);
-        u.setAuthorities(List.of());
-        SecurityContextHolder.getContext().setAuthentication(
-                new UsernamePasswordAuthenticationToken(u, null, List.of()));
-
-        BusinessException e = assertThrows(BusinessException.class,
-                () -> controller.page(1, 20, null, null, null, null));
-
-        assertEquals(403, e.getCode());
-        verify(orderMapper, never()).selectPage(any(), any());
-    }
-
     // ─────────────── 退款在途标记:列表/详情出口处补的那个派生量 ───────────────
     //
     // 用户 2026-09-30 报的:「我点击退款后,lsy 回调送达前,页面上还有退款按钮,难道还能重复点吗?」
@@ -499,7 +427,6 @@ class LeshouyingOrderControllerTest {
     @Test
     void page_只有退款在途的那一行才标记为退款中() {
         Page<LsyOrderEntity> p = pageOfTwoOrders();
-        login(3L, false);   // /page 要求可识别的主体(创建人闸)
 
         controller.page(1, 20, null, null, null, null);
 
@@ -512,7 +439,6 @@ class LeshouyingOrderControllerTest {
     @Test
     void 退款在途的判据_与后端闸门同一集合() {
         pageOfTwoOrders();
-        login(3L, false);   // /page 要求可识别的主体(创建人闸)
 
         controller.page(1, 20, null, null, null, null);
 
@@ -548,7 +474,6 @@ class LeshouyingOrderControllerTest {
 
     @Test
     void page_空页_一次退款表都不查() {
-        login(3L, false);   // /page 要求可识别的主体(创建人闸)
         when(orderMapper.selectPage(any(), any())).thenReturn(new Page<>(1, 20));
 
         controller.page(1, 20, null, null, null, null);