From f3ea06c154ddea917258e04d3adc2417096cc3e7 Mon Sep 17 00:00:00 2001 From: lili Date: Sun, 21 Jun 2026 13:19:16 -0700 Subject: [PATCH] =?UTF-8?q?fix(account):=20=E4=BF=AE=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E8=B5=84=E6=96=99=E6=9B=B4=E6=96=B0=20jsonb?= =?UTF-8?q?=20=E7=B1=BB=E5=9E=8B=E9=94=99=E8=87=B4=20update=20500(?= =?UTF-8?q?=E5=8F=8D=E5=81=87=E7=BB=BF=E6=8C=96=E5=87=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AccountProfileMapper.updateByAccountUserIdAndVersion 用 LambdaUpdateWrapper.set 写 profile_snapshot(PG jsonb 列),默认不走 DO 上 @TableField 的 JsonbStringTypeHandler, 按 varchar 绑定 →「column is jsonb but expression is character varying」→ update 500 (insert 走 typeHandler 故首次 create 侥幸 OK、对已存在 profile 的 update 必 500)。 set 显式指定 JsonbStringTypeHandler 与 insert 路径对齐。 新增 account-profile-update.spec.ts(真后端写路 e2e:改公开署名→PATCH /profile→ code:0+version 乐观锁自增+UI 回显;只改署名不动昵称以不破坏 live-read 的 nickname 断言) 正是它暴露此 bug——curl 实证旧 jar PATCH 500、rebuild 后 code:0。全量 e2e 39/0。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../mysql/account/AccountProfileMapper.java | 8 ++- .../e2e/account-profile-update.spec.ts | 58 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 muse-studio/e2e/account-profile-update.spec.ts diff --git a/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/mysql/account/AccountProfileMapper.java b/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/mysql/account/AccountProfileMapper.java index 9a97794f..2a9378d7 100644 --- a/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/mysql/account/AccountProfileMapper.java +++ b/muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/mysql/account/AccountProfileMapper.java @@ -3,6 +3,7 @@ package cn.iocoder.muse.module.member.dal.mysql.account; import cn.iocoder.muse.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.muse.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.muse.module.member.dal.dataobject.account.AccountProfileDO; +import cn.iocoder.muse.module.member.dal.type.JsonbStringTypeHandler; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.apache.ibatis.annotations.Mapper; @@ -33,7 +34,12 @@ public interface AccountProfileMapper extends BaseMapperX { .set(AccountProfileDO::getAvatarUrl, profile.getAvatarUrl()) .set(AccountProfileDO::getPublicPenName, profile.getPublicPenName()) .set(AccountProfileDO::getBio, profile.getBio()) - .set(AccountProfileDO::getProfileSnapshot, profile.getProfileSnapshot()) + // profile_snapshot 是 PG jsonb 列:LambdaUpdateWrapper.set 默认不走 DO 上 @TableField 的 + // typeHandler,会按 varchar 绑定 → 报「column is jsonb but expression is character varying」 + // 致 update 500(insert 走 @TableField typeHandler 故无此问题)。显式指定 JsonbStringTypeHandler + // 与 insert 路径对齐,jsonb 列才能正确写入。 + .set(AccountProfileDO::getProfileSnapshot, profile.getProfileSnapshot(), + "typeHandler=" + JsonbStringTypeHandler.class.getName()) // profileVersion 是乐观锁条件,命中后由数据库原子自增,避免并发覆盖。 .setSql("profile_version = profile_version + 1") .eq(AccountProfileDO::getAccountUserId, profile.getAccountUserId()) diff --git a/muse-studio/e2e/account-profile-update.spec.ts b/muse-studio/e2e/account-profile-update.spec.ts new file mode 100644 index 00000000..ee105e2f --- /dev/null +++ b/muse-studio/e2e/account-profile-update.spec.ts @@ -0,0 +1,58 @@ +import { expect, test, type Page } from '@playwright/test'; + +/** + * 账户中心「保存资料」写路 e2e(真实后端,MSW off)。 + * + * 前置:vite VITE_API_MOCK=false、真实 muse-server 48080。 + * 目的:验个人中心资料更新写路真打后端——PATCH /profile(带 expectedVersion 乐观锁 + commandId 幂等), + * 改公开署名→真后端落库→响应回带新值→概览区渲染更新(反假绿:写路+乐观锁+幂等+回显)。 + * 设计: + * - 只改「公开署名」、不动「昵称」——activeNickname 回退到 profile.nickname,故 nickname 保持不变, + * 不破坏 live-read.spec 对 nickname='活体用户' 的断言。 + * - 唯一署名值每次产新内容(断言才真证写生效,非 pre-existing 假绿); + * - expectedVersion 取自 goto 时 GET /profile 的最新 version,commandId 每次唯一,故可重复跑不被乐观锁拦。 + */ +const TOKEN = 'test1'; + +async function seedToken(page: Page): Promise { + await page.addInitScript((t) => { + window.localStorage.setItem('accessToken', t); + window.localStorage.setItem('tenantId', '1'); + }, TOKEN); +} + +test('账户中心保存资料→真打 PATCH /profile→公开署名更新(真实后端)', async ({ page }) => { + await seedToken(page); + + // 进个人中心,先等资料读取(拿到当前 version,供乐观锁更新)。 + const [profileResp] = await Promise.all([ + page.waitForResponse( + (r) => /\/profile$/.test(new URL(r.url()).pathname) && r.request().method() === 'GET', + { timeout: 15_000 } + ), + page.goto('/account'), + ]); + expect(profileResp.status()).toBe(200); + + // 唯一公开署名:每次产新值,断言才能真证写生效(非 pre-existing 假绿)。只改署名、不动昵称。 + const uniquePen = `署名·e2e-${Date.now()}`; + await page.getByPlaceholder('公开署名').fill(uniquePen); + + // 点「保存资料」→ 等真实 PATCH /profile 写入响应(带 expectedVersion 乐观锁 + commandId 幂等)。 + const [patchResp] = await Promise.all([ + page.waitForResponse( + (r) => /\/profile$/.test(new URL(r.url()).pathname) && r.request().method() === 'PATCH', + { timeout: 15_000 } + ), + page.getByRole('button', { name: '保存资料' }).click(), + ]); + + // Yudao 口径:成功为 HTTP 200 + CommonResult{code:0},data 回带更新后的公开署名。 + expect(patchResp.status()).toBe(200); + const body = await patchResp.json(); + expect(body.code).toBe(0); + expect(body.data?.publicPenName).toBe(uniquePen); + + // 概览区(profile.publicPenName)经 onSuccess setQueryData 更新为新署名(证 UI 回显真后端写结果)。 + await expect(page.getByText(uniquePen)).toBeVisible({ timeout: 15_000 }); +});