diff --git a/docs/updata_sql.md b/docs/updata_sql.md index 309d3fd..14bab01 100644 --- a/docs/updata_sql.md +++ b/docs/updata_sql.md @@ -6,6 +6,40 @@ --- +## 头像库「是否默认」唯一化:`tt_user_avatar` 存量数据清洗 + +**背景**:`tt_user_avatar.is_default` 的语义是「新用户 / 白号注册时分配的初始头像」, +**全表必须最多只有一条 `is_default='1'`**。但后台「设置 → 头像管理」的开关写成默认时, +后端只做了 `updateById`,没有把其他记录的默认标记清掉 ⇒ 库中一度累积出 **7 条默认**(id 63~69), +`id=62` 反而是「否」。后果:后台看到一排「是」容易误解(用户 2026-09-24 反馈), +且 C 端注册处(`ApiRegisterServiceImpl` / `PromoLoginServiceImpl`)用的是 +`list(is_default='1')` + `RandomUtil.choice(...)` **随机挑一条**,导致默认头像实际不唯一、每次注册拿到的都不同。 + +**代码侧修复**(非 SQL,见 `docs/修改记录.md` 同日条目):`TtUserAvatarServiceImpl` +在 `updateUserAvatarById` / `saveAvatar` 中增加互斥逻辑 —— 任何把某条置为 `is_default='1'` 的写入, +都先把其余记录清成 `'0'`,两步同一事务。 + +**本次数据库变更 1 处**(2026-09-24 已在库 `henan` 执行):把多余的默认标记收敛为**只保留 id 最小的一条**(id=63)。 + +**变更 SQL**: + +```sql +UPDATE `tt_user_avatar` + SET `is_default` = '0' + WHERE `is_default` = '1' + AND `id` <> (SELECT `min_id` FROM ( + SELECT MIN(`id`) AS `min_id` FROM `tt_user_avatar` WHERE `is_default` = '1' + ) AS `t`); +``` + +> ⚠️ MySQL 不允许在 `UPDATE` 的子查询里直接查同一张表,故此处包了一层派生表 `t` 绕过; +> 该写法的坑已记在 `.workbuddy/memory/` 当日日志里,后续同类 SQL 照此办理。 + +**验证口径**:执行后 `SELECT is_default, COUNT(*) FROM tt_user_avatar GROUP BY is_default;` +应当只有两行 —— `0 → N`(N = 总数-1)、`1 → 1`。 + +--- + ## 幸运升级·水位策略简化为「单一目标水位」:`sys_config` 配置结构变更 **背景**:上一版把水位策略做成了 **4 档波段表**(每档 = 水位下界/上界 + 随机数下界/上界), diff --git a/docs/修改记录.md b/docs/修改记录.md index 084be9d..a41ec19 100644 --- a/docs/修改记录.md +++ b/docs/修改记录.md @@ -9,6 +9,53 @@ ## 2026-09-24 +- **头像管理:「是否默认」可同时多条为「是」的缺陷修复(唯一性约束 + 存量数据清洗 + 开关交互)** + + **起因**:用户在「网站设置 → 头像管理」截图反馈,「是否默认」那一列有 7 条都显示绿色「是」, + 而按语义**全站默认头像只应该有一条**。 + + **排查结论(逐层验证,不是猜的)**: + + | 层 | 实测 | 结论 | + |---|---|---| + | 数据库 | `SELECT is_default, COUNT(*) ... GROUP BY is_default` → `0→1`、`1→7`(id 63~69) | **数据本身就错**,界面是忠实反映 | + | 前端 | `v-model="scope.row.isDefault"` 直读接口字段 | 无显示层问题,不需要改渲染 | + | 后端 | `TtUserAvatarServiceImpl.updateUserAvatarById` 只做 `updateById` | **根因**:设为默认时不互斥清空其他记录 | + + ⇒ 定位为**后端写接口缺约束 + 存量脏数据**,不是 UI bug。 + + **二、连带发现(比表面问题更严重)**:C 端有 4 处消费「默认头像」,其中 + `ApiRegisterServiceImpl`、`PromoLoginServiceImpl` 用的是 + `list(is_default='1')` + `RandomUtil.choice(...)` **随机挑一条** —— 这是在「可能有多条默认」的 + 既成事实上打的补丁。后果是**每次注册拿到的初始头像都不一样**,默认头像形同虚设。 + (`TtUserServiceImpl` 白号那边已有 `defaultAvatarList.isEmpty()` 的降级,逻辑本身没问题。) + + **三、改动清单** + + 1. **后端互斥(核心)** `skins-service/service-admin`: + - `TtUserAvatarService` 新增 `saveAvatar(TtUserAvatar)`、`getDefaultAvatar()`; + - `TtUserAvatarServiceImpl` 新增私有方法 `clearOtherDefaultsIfNeeded(...)`: + 凡是把某条写成 `is_default='1'` 的写入,**先 `UPDATE ... SET is_default='0' WHERE is_default='1' AND id<>当前id`**, + 再落当前这条,两步同一事务(`@Transactional`)。只在确实要置默认时才执行 UPDATE,普通编辑不白扫全表。 + - `updateUserAvatarById` / `saveAvatar` 均接入;顺手删掉原方法里无意义的 `setAvatar(getAvatar())`。 + 2. **控制器** `TtUserAvatarController.add` 由 `userAvatarService.save(...)` 改走 `saveAvatar(...)`, + 使**新增**也受同一约束(原来新增一条默认就会叠加出第二个)。 + 3. **存量数据清洗**:`UPDATE tt_user_avatar SET is_default='0' WHERE is_default='1' AND id <> (最小 id 的默认)`, + 实测影响 6 行,清洗后 `0→7 / 1→1`(保留 id=63)。SQL 全文见 `docs/updata_sql.md`。 + 4. **前端交互** `ruoyi-ui/src/views/skins/ttSetting/avatar.vue`:「是否默认」由 `updateUserStatus` + 改走新的 `updateDefault(...)` —— 开启时先 `$confirm` 告知「原默认头像「xx」将自动取消默认」, + 取消则回滚开关视觉并重新拉列表。关闭时直接保存。 + + **四、验证**:后端 `mvn -q -pl skins-service/service-admin -am install -DskipTests` 通过, + 字节搜 `clearOtherDefaultsIfNeeded`/`saveAvatar`/`getDefaultAvatar` 均在 `.class` 里; + 前端 `vue-cli-service build` 通过,新文案与新方法名均落在 `dist/static/js/chunk-cf2f9538.*.js`。 + + **五、踩坑**:MySQL **不允许 `UPDATE` 的子查询直接查同一张表**(报 1093), + 解法是包一层派生表 `(SELECT MIN(id) ... ) AS t` 绕过 —— 后续同类 SQL 照此办理。 + + ⚠️ **待办**:后端 jar 未重打包、服务未重启,互斥逻辑**尚未在运行中的服务里生效**; + 前端 `dist` 已就绪但线上资源需按部署流程发布。存量数据已清洗,界面当下已正确。 + - **幸运升级:水位配置从「4 档波段表」简化为「一个目标水位百分比」+ 饰品随机数区间恢复可编辑** **起因**:上一版后台要维护一张 4 档波段表(水位下界/上界 × 随机数下界/上界 = 16 个数), @@ -199,6 +246,113 @@ 这是「水位是累计比例」的必然结果 —— 想在大水位下也能快速纠偏,要调 **`gain`**(水位每偏离 1 个点、 中心移动的幅度;现在默认 2,暂未在后台暴露),或者直接人工注水/抽水。 +- **后台开关(el-switch)统一为「文字 = 当前值」口径,修掉反向文字露底色的误导** + + **起因**:用户反馈「头像管理」的**状态**与**是否默认**两列看得费解。查下来是两个叠加问题: + + 1. **两边文字显示规则相反**。element-ui 的 `el-switch` 里 `active-text` 渲染在**右**侧、 + 且只在**开启时**显示;`inactive-text` 渲染在**左**侧、只在**关闭时**显示。 + 本页「状态」用的是 `inactive-text="启用"`(把当前值放在左侧), + 「是否默认」用的是 `active-text="是"`(把当前值放在右侧)—— 同一张表里两套相反的读法。 + 2. **更糟的是反向文字会露出底色**。「状态」列开启时左侧露出绿色底 + 白字「启用」, + 点一下滑块滑到左边、绿底被盖住,可读文字变成「停用」。两列的滑块方向可以完全相反, + 同一行的绿白视觉还不一样(`is-checked` 与背景色是两套独立配色)。 + + ⇒ **定的新口径(全后台统一):`inactive-text` = 关闭时的值,`active-text` = 开启时的值, + 也就是「文字显示的就是当前值」,滑块停在哪边、哪边的文字就是现在的结果。** + 颜色同步收敛为与 `active-value`/`inactive-value` 同侧:**绿 = 该项生效、红 = 该项未生效**。 + + **改动清单(7 个文件,15 处开关)** + + | 文件 | 列 / 字段 | 调整 | + |---|---|---| + | `skins/ttSetting/avatar.vue` | 状态 `status` | 文字翻正(启用/停用),配色与 `active-value` 同侧 | + | `skins/ttSetting/avatar.vue` | 是否默认 `isDefault` | `active/inactive-value` 翻正为 `1/0`(= 与 `initData`、弹窗单选按钮一致),文字归右侧 | + | `skins/ttuser/index.vue` | 帐号状态 `status` | `active/inactive-value` 翻正为 `0/1`(原为 `1/0`,是全局唯一的反向异类) | + | `skins/ttuser/index.vue` | 提货状态 `deliveryStatus` | 同上翻正(`1` = 已禁用提货) | + | `skins/ttuser/index.vue` | 赠送状态 `giftStatus` | 同上翻正(`1` = 已禁用赠送) | + | `skins/ttBonus/index.vue` | 启用状态 `status` | 文字从「是/否」改为「启用/禁用」并翻正 | + | `skins/ttBox/boxType.vue` | 是否对战类型 `isFightType`、是否显示 `isShow` | label 归位;`isShow` 的 `active/inactive-value` 翻正为 `1/0` | + | `skins/ttredPacket/index.vue` | 是否启用 `useStatus` | `active/inactive-value` 翻正为 `1/0`,配色同侧 | + | `skins/ttUpgrade/index.vue` | 状态 `status` | `active/inactive-value` 翻正为 `0/1`(原为 `1/0`)+ 文字翻正 | + | `skins/ttBox/index.vue` | 弹窗内 是否对战宝箱 `isFight`、宝箱状态 `status`、是否首页推荐 `isHome` | 文字归位、`isFight`/`status` 的 `active-value` 翻正 | + + **关键约束:以上全部只是「值 ↔ 显示」的映射调整,`active-value` 与 `inactive-value` 成对同翻, + 提交给后端的字段值逐字节不变,数据库语义与接口契约零改动**(各页 `@change` 都是把整个 row/form + 原样 `PUT` 出去,不做值转换)。 + + **语义校对依据**(改前逐个确认过,避免把含义改反): + - `tt_user_avatar.status` 与 `tt_user.status`:`UserStatus` 枚举 = `OK("0","正常")` / `DISABLE("1","停用")`, + `ApiUserDetailsServiceImpl` 对 `DISABLE` 直接拒登并踢线 ⇒ **`0` = 启用**。 + - `tt_user_avatar.is_default`:`TtUserAvatar` 的 `@Excel(readConverterExp = "0=否,1=是")`, + 且注册 / 白号 / 登录三处均查 `is_default = '1'` 取默认头像 ⇒ **`1` = 是**。 + - `tt_user.delivery_status` / `gift_status`:实体注释 + 业务代码均为 **`1` = 已禁用**(`"1".equals(...)` 返回"已被禁用"提示)。 + - 可对照的既有正确写法:`ttSetting/banner.vue`、`ttRechargeList/index.vue`、`ttuser/userPack.vue`。 + + ⚠️ **踩坑**:用户截图里「是否默认」列显示为**红底白字「否」**容易被误读成"开着"。 + 那是 `active-color`/`inactive-color` 与 `active-text`/`inactive-text` 配成反的典型症状 —— + 改完须确认**颜色所在的半边和文字所在的半边是同一半**。 + + ⚠️ **未改(故意留着)**:`ttRoll/officialRollConfig.vue` 的状态开关用的是 + `:value="scope.row.status === '0'"` + 无文字标注的正向布尔(绿=开),本身自洽; + 只建议后续补一个文字标注。 + +- **管理后台:3 个页面表格右侧留白(「表格没有平铺完整」)** + + **起因**:用户反馈「管理后台有些页面的表格没有平铺完整」。追问确认现象是 + **表格右侧留一条空白**(不是列错位、也不是横向滚动条)。 + + **根因**(读 Element 源码定论):`element-ui/packages/table/src/table-layout.js:132-195` + 的 `updateColumnsWidth()` —— + + ``` + flexColumns = 所有「width 不是数字」的列(含显式 min-width 列、以及两者都没写的列) + if (flexColumns.length > 0 && fit) → 自动分配,表格撑满 bodyWidth (第 145 行) + else → bodyWidth = Σ(width || minWidth || 80)(第 182-194 行) + ``` + + ⇒ **只要有一列写了 `min-width`(或宽度两者都不写),表格就会自动撑满容器; + 但当所有列都是固定 `width` 时,表格宽度恒等于列宽之和 —— 容器再宽也不拉伸,右侧就空一条。** + + 同一份源码里还有个易漏的点:`type="selection" / "expand" / "index"` 在未写 width 时, + 由 `config.js:3-26` 的 `cellStarts` 给死 **48px 固定宽**,**不参与弹性分配** —— + 所以「展开列 + 其余全固定宽」的表格同样会留白。 + + **扫描结论**(115 个顶层表格,1920 视口下容器可用 1584px): + + - **仅 6 个**是全固定列(会留白);**109 个**含弹性列、会自行撑满。 + - **0 个**会出横向滚动条(判据:`Σwidth + Σmin-width > 容器`)。 + - 留白量**随屏幕变宽而增大**(2560 视口下最大 1854px)—— 这正是"没平铺完整"的典型特征。 + + **改动清单(3 个页面主表格,各把「内容最长的固定列」改为 `min-width`)** + + | 文件 | 列 | 调整 | 表格宽 → 结果 | + |---|---|---|---| + | `skins/blankCardKey/index.vue` | 卡密编码 `cardCode` | `width="200"` → `min-width="200"` | 1000 → 撑满(原留白 584px) | + | `system/role/index.vue` | 权限字符 `roleKey` | `width="150"` → `min-width="150"` | 1075 → 撑满(原留白 509px) | + | `game/wheel/records/index.vue` | 投注符号 `betSymbols` | `width="140"` → `min-width="140"` | 1368 → 撑满(原留白 216px) | + + 选列理由:卡密编码是 32 位等宽字符 + 复制按钮、权限字符内容最长且**已带 `show-overflow-tooltip`** + (加宽无副作用)、投注符号是拼接字符串、长度波动最大 —— 都是"多给宽度只有好处"的列。 + 三处均加注释说明「为什么必须是 min-width」,防止后来人按"列宽统一用 width"的惯例改回去。 + + **未改(用户明确不动)**:3 个展开行内嵌明细表(`wheel/records` 下注明细、 + `delivery/transferApply` 与 `transferApplyProcessed` 的饰品明细)与 1 个弹窗内倍率表 + (`wheel/config`,固定 `width:640px`)—— 它们宽度受容器约束是设计意图。 + 其中 `transferApply` / `transferApplyProcessed` 声明了 `style="width: auto"` 却只有 2 列固定宽 + (300+120=420),**在展开行里只占 420px**;若后续要铺满,改其中一列为 `min-width` 即可。 + + **验证**: + - 编译产物断言:`vue-template-compiler` 编译 3 个页面,✓ 全部通过, + `min-width` 各 1 个(200 / 150 / 140),其余列仍为固定 width。 + - 算法级回归:复现 `updateColumnsWidth` 后, + **1280 / 1440 / 1600 / 1920 / 2560 五个视口下 3 个主表格留白全部归零**。 + - dev server 起来后 `curl` 返回 200,确认改动未破坏编译。 + - ⚠️ **本机无法真机截图核对**:助手拉起 Chrome 被安全策略拦(连带报 `wsl.exe` 在程序黑名单), + 故本次验证口径是「读源码 + 复现算法 + 编译产物断言」,**未经浏览器实测**。 + + **纯前端改动,不涉及数据库与接口**;`dist` 未发布。 + --- ## 2026-09-23 diff --git a/ruoyi-ui/src/assets/styles/index.scss b/ruoyi-ui/src/assets/styles/index.scss index f7fe363..3ac4740 100644 --- a/ruoyi-ui/src/assets/styles/index.scss +++ b/ruoyi-ui/src/assets/styles/index.scss @@ -255,3 +255,5 @@ aside { // 皮肤层放最后,保证覆盖 Element UI 默认样式 @import './skin.scss'; +// 开关(状态列)已改为自研组件 components/CapsuleSwitch(自带 scoped 样式), +// 不再需要历史上那种全局 ::v-deep 覆盖 el-switch 内部结构的写法。 diff --git a/ruoyi-ui/src/main.js b/ruoyi-ui/src/main.js index 04c673a..ee73cc1 100644 --- a/ruoyi-ui/src/main.js +++ b/ruoyi-ui/src/main.js @@ -33,6 +33,8 @@ import ImageUpload from "@/components/ImageUpload" import ImagePreview from "@/components/ImagePreview" // 字典标签组件 import DictTag from '@/components/DictTag' +// 胶囊开关组件(替代 el-switch) +import CapsuleSwitch from '@/components/CapsuleSwitch' // 头部标签组件 import VueMeta from 'vue-meta' // 字典数据组件 @@ -58,6 +60,7 @@ Vue.component('Editor', Editor) Vue.component('FileUpload', FileUpload) Vue.component('ImageUpload', ImageUpload) Vue.component('ImagePreview', ImagePreview) +Vue.component('CapsuleSwitch', CapsuleSwitch) Vue.use(directive) Vue.use(plugins) diff --git a/ruoyi-ui/src/views/game/sugar/dailywin/index.vue b/ruoyi-ui/src/views/game/sugar/dailywin/index.vue index a781900..eec3bab 100644 --- a/ruoyi-ui/src/views/game/sugar/dailywin/index.vue +++ b/ruoyi-ui/src/views/game/sugar/dailywin/index.vue @@ -129,70 +129,6 @@ export default { }; \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttBox/boxType.vue b/ruoyi-ui/src/views/skins/ttBox/boxType.vue index 263030c..0f09064 100644 --- a/ruoyi-ui/src/views/skins/ttBox/boxType.vue +++ b/ruoyi-ui/src/views/skins/ttBox/boxType.vue @@ -33,30 +33,26 @@ @@ -213,68 +209,4 @@ export default { .home { padding: 20px; } -::v-deep .el-switch { - border-radius: 0px !important; - height: 24px !important; -} -::v-deep .el-switch__core { - width: 70px !important; - height: 24px; - border-radius: 3px; - border: none; -} -::v-deep .el-switch__core::after { - width: 50%; - height: 24px; - border-radius: 0px; - top: 0px !important; - right: 0px !important; - left: 0px; - border: 1px solid #ccc; - border-radius: 3px; -} -::v-deep .el-switch.is-checked .el-switch__core::after { - margin-left: -35px; - box-sizing: border-box; - top: 0px !important; - right: 0px !important; - border: 1px solid #ccc; - border-radius: 3px; - text-align: center; -} -/*关闭时文字位置设置*/ -::v-deep .el-switch__label--right { - text-align: center; - position: absolute; - width: 40%; - z-index: 1; - right: 6px; - margin-left: 0px; - color: rgba(255, 255, 255, 1); - span { - font-size: 13px; - box-sizing: border-box; - color: #fff; - } -} -/* 激活时另一个文字消失 */ -::v-deep .el-switch__label.is-active { - text-align: center; - display: none; -} -/*开启时文字位置设置*/ -::v-deep .el-switch__label--left { - text-align: center; - position: absolute; - width: 40%; - z-index: 1; - left: 5px; - margin-right: 0px; - color: rgba(255, 255, 255, 1); - span { - font-size: 13px; - box-sizing: border-box; - color: #fff; - } -} \ No newline at end of file diff --git a/ruoyi-ui/src/views/skins/ttBox/index.vue b/ruoyi-ui/src/views/skins/ttBox/index.vue index a8e6796..7d899e8 100644 --- a/ruoyi-ui/src/views/skins/ttBox/index.vue +++ b/ruoyi-ui/src/views/skins/ttBox/index.vue @@ -320,16 +320,14 @@ - + /> @@ -905,70 +899,6 @@ export default { }; diff --git a/ruoyi-ui/src/views/system/role/index.vue b/ruoyi-ui/src/views/system/role/index.vue index 39b779c..79eb8e2 100644 --- a/ruoyi-ui/src/views/system/role/index.vue +++ b/ruoyi-ui/src/views/system/role/index.vue @@ -101,7 +101,10 @@ - + +