fix(account): 修个人中心资料更新 jsonb 类型错致 update 500(反假绿挖出)
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) <noreply@anthropic.com>
This commit is contained in:
parent
366ed63544
commit
f3ea06c154
@ -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<AccountProfileDO> {
|
||||
.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())
|
||||
|
||||
58
muse-studio/e2e/account-profile-update.spec.ts
Normal file
58
muse-studio/e2e/account-profile-update.spec.ts
Normal file
@ -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<void> {
|
||||
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 });
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user