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:
lili 2026-06-21 13:19:16 -07:00
parent 366ed63544
commit f3ea06c154
2 changed files with 65 additions and 1 deletions

View File

@ -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())

View 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 });
});