From 4cf1d4120addce898b9b36b6d5a288b7c9ac79fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BC=96=E7=A0=81=E5=B7=A5=E7=A8=8B=E5=B8=88?= Date: Sun, 16 Aug 2026 01:07:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(pc-client,account-service):=20B-61=20?= =?UTF-8?q?=E8=AF=95=E7=94=A8=E5=89=8D=E5=B0=8F=E4=BF=AE=20-=20=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E5=8F=96=E9=A6=96=E5=AD=97/=E7=BE=A4=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E6=8B=BC=E6=A0=BC/=E8=BF=91=E4=BC=BC=E7=81=B0Token?= =?UTF-8?q?=E6=94=B6=E6=95=9B/=E9=87=8D=E7=BD=AE=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E8=B8=A2=E4=B8=8B=E7=BA=BF=EF=BC=8C=E6=9E=84=E5=BB=BA=20v1.0.1?= =?UTF-8?q?=20=E5=AE=89=E8=A3=85=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- account-service/server.js | 1 + docs/phase4-launch-checklist.md | 131 ++++++++++++++++++ pc-client/README.md | 3 + pc-client/package-lock.json | 24 +--- pc-client/package.json | 4 +- pc-client/package_electron.json | 2 +- pc-client/src/components/OIMAvatar/index.tsx | 32 ++++- pc-client/src/config/index.ts | 2 +- .../ConversationSider/ConversationItem.tsx | 2 +- .../pages/chat/ConversationSider/index.tsx | 2 +- pc-client/src/pages/chat/EmptyChat.tsx | 2 +- .../src/pages/chat/queryChat/ChatContent.tsx | 2 +- .../pages/chat/queryChat/ChatFooter/index.tsx | 2 +- .../queryChat/GroupSetting/GroupSettings.tsx | 12 +- .../chat/queryChat/SingleSetting/index.tsx | 6 +- .../src/pages/common/GroupCardModal/index.tsx | 2 +- .../pages/common/RtcCallModal/RtcLayout.tsx | 2 +- pc-client/src/pages/contact/ContactSider.tsx | 2 +- pc-client/src/styles/global.scss | 4 + 19 files changed, 194 insertions(+), 43 deletions(-) create mode 100644 docs/phase4-launch-checklist.md diff --git a/account-service/server.js b/account-service/server.js index 5a21e83..8a6ae6b 100644 --- a/account-service/server.js +++ b/account-service/server.js @@ -326,6 +326,7 @@ const routes = { upsertEmployee(body.staffNo, emp.name, String(body.password)); if (employees[body.staffNo].status !== 'active') employees[body.staffNo].status = emp.status; // 保留停用状态 saveStore(); + forceLogoutAll(body.staffNo); // 密码已改,立即踢掉该员工所有端,防止旧 token 继续使用 ok(res, publicView(employees[body.staffNo], body.staffNo)); }, }; diff --git a/docs/phase4-launch-checklist.md b/docs/phase4-launch-checklist.md new file mode 100644 index 0000000..b06ea0b --- /dev/null +++ b/docs/phase4-launch-checklist.md @@ -0,0 +1,131 @@ +# 第4步 · 试用前部署与验收清单(B-61) + +> 本文件记录"50 人试用"前必须在服务器上手动完成的事项、验收清单与手机正式签名流程。 +> 仓库代码与本文件已就绪;**下列所有"服务器上做"的动作都要等白栖知确认后再执行**,本轮不直接改现网。 + +## 1. 必须由白栖知确认后执行的服务器操作 + +### 1.1 管理员强口令(必做,试用前) + +账号服务的管理页 `http://<服务器IP>:10010/` 现在默认口令是 `admin123`,谁都能改账号,**必须改掉**。 + +在服务器仓库目录(`tongxunruanjian/`)里: + +```bash +# 1) 把 .env 里的管理口令改成一个自己记得住的强口令 +# 编辑 .env,找到 ACCOUNT_ADMIN_TOKEN=admin123,改成如:ACCOUNT_ADMIN_TOKEN=Changlian@2026 +# 2) 重建账号服务使生效 +docker compose up -d --build account-server +``` + +验证:浏览器打开管理页,用新口令能登录;旧口令 `admin123` 提示错误。 + +### 1.2 导入 50 人名单并确认持久化(必做) + +账号数据存在服务器 `tongxunruanjian/data/account/employees.json`(已挂载卷,重建容器不丢)。 +质检审核发现现网账号库目前只有 2 个测试账号,试用前需重新导入 50 人真实名单。 + +步骤: +1. 准备 CSV:每行 `工号,姓名,初始密码`(Excel 可先存 CSV)。工号限字母/数字/`-`/`_`,2-32 位;密码至少 6 位。 + 管理页有"模板下载"按钮,直接按模板填。 +2. 打开管理页 → 粘贴/选择 CSV 导入,确认导入成功数 = 50。 +3. 到服务器确认文件真落盘了: + ```bash + ls -l tongxunruanjian/data/account/employees.json + grep -c '"hash"' tongxunruanjian/data/account/employees.json # 应为 50 + ``` +4. 抽查:用某个员工的工号+密码在手机端/电脑端各登录一次。 + +> 注意:导入的 50 人名单 = 真实员工数据,**必须等白栖知明确同意后再导**。 + +### 1.3 手机正式签名(试用分发前准备,可由用户保管) + +当前手机端 release 包用的是调试签名,之后换正式签名会导致已装用户要卸载重装,所以趁试用期只有小范围用户,先切到正式签名。 + +签名密钥**绝不提交仓库**,由白栖知自行保管。流程(一次性准备,在开发机上做): + +```bash +# 1) 生成自签证书(keytool 随 JDK 自带),口令自己设并记牢,别忘 +keytool -genkeypair -v -keystore changlian-release.jks \ + -alias changlian -keyalg RSA -keysize 2048 -validity 36500 \ + -storepass <你的口令> -keypass <你的口令> \ + -dname "CN=Changlian, OU=IT, O=Company, L=City, ST=State, C=CN" + +# 2) 把密钥库放到安全位置(建议不放代码目录),并在本机新建 +# mobile/android/key.properties(已被 .gitignore 忽略,不会入库): +# storePassword=<你的口令> +# keyPassword=<你的口令> +# keyAlias=changlian +# storeFile= + +# 3) mobile/android/app/build.gradle.kts 的 release 块已留 TODO: +# signingConfig 目前指向 debug,改成读 key.properties 的正式签名后, +# 执行 flutter build apk --release 即可出正式签名包。 +``` + +参考 `mobile/android/app/build.gradle.kts` release 块注释(R8 混淆必须保持关闭,与官方样板一致,OpenIM 插件靠反射分发调用,混淆会静默失效)。 + +### 1.4 外网访问(按需,需白栖知明确确认) + +- 若只内网试用:跳过本节。 +- 若要让手机流量也能用:需要 ① 公网 IP 或域名 ② ICP 备案 ③ 反代 + HTTPS 证书(服务器上 nginx/caddy 都可,把 :10001/:10002/:10005/:10010/:17880 反向代理到外网域名)。**这一步必须先经白栖知拍板再动,且涉及实名备案资料,无法代办。** + +## 2. 真机 + 跨端回归清单(试用前执行,逐项打勾) + +> 说明:本轮在开发环境已完成**代码级验证**(见下方"已完成的自测");下表为需两台真机/PC 参与的联调项,由白栖知组织 1~2 人配合执行。 + +### A. 登录与基础 +- [ ] 手机(安卓)装 `changlian-1.0.5-android-arm64.apk`,工号+密码能登录 +- [ ] 电脑装 `畅联_1.0.1.exe`,同一工号+密码能登录 +- [ ] 两个不同工号同时登录,互不干扰 +- [ ] 输错密码 3 次有提示(登录接口无失败限制,仅提示) + +### B. 文字消息(手机↔PC 互发) +- [ ] 手机发文字 → PC 实时收到,排序正确 +- [ ] PC 发文字 → 手机实时收到 +- [ ] 长文本(几百字)、特殊字符(emoji、括号、引号)不丢字不乱码 +- [ ] 群聊:建一个 3 人群,三端消息同步 + +### C. 语音消息 +- [ ] 手机发语音条 → PC 能听到,时长正确,已听红点消失 +- [ ] PC 发语音条 → 手机能听到 +- [ ] 语音条连续发多条,顺序不乱 + +### D. 文件 +- [ ] 手机传文件(如 1 张图片)→ PC 能下载打开 +- [ ] PC 传文件 → 手机能下载打开 +- [ ] 大文件(≥50MB,OpenIM/MinIO 上限内)传输成功 +- [ ] 断网后发送显示红色感叹号,点重发能成功 + +### E. 语音通话 +- [ ] 手机 ↔ PC 一对一语音通话接通,声音双向清晰 +- [ ] 挂断正常,会话列表不残留"通话中" +- [ ] 手机断网/切后台再回,通话状态不假死 + +### F. 管理端 +- [ ] 管理员停用一个账号 → 该账号手机/PC 立即被踢下线、无法再登录 +- [ ] 管理员重置某账号密码 → 该账号已登录端被踢下线,新密码能登录(本轮已加"重置密码踢下线",需真实验证一次) +- [ ] 管理页能翻看 50 人列表 + +## 3. 已完成的自测记录(本轮,代码层验证) + +- **PC 安装包**:`pc-client` 全新 `npm install` 通过(husky 在非 ASCII 路径下不再阻断,回退提示中文);`npm run build` exit 0;`npx electron-builder --win --x64` 产出 `畅联_1.0.1.exe`(约 101MB);`win-unpacked` 双击启动正常(冒烟通过)。 +- **类型检查**:`tsc --noEmit` 24 个错误,与基线 `2fe8417` 完全一致(均为工程原有问题),本次改动未新增。 +- **头像/拼格/Token 收敛**:改动已进 `dist`(构建产物中可查到 `v1.0.1`、`avatar-gray`、`chat-bg`、`call-wait-bg`)。 +- **重置密码踢下线**:起本地账号服务(临时数据文件,连测试 OpenIM)验证——员工登录拿到 token → 管理端重置密码 → 原 token 再调 OpenIM `parse_token` 返回 `TokenKickedError`;旧密码登录失败、新密码登录成功。✓ + +## 4. 改动清单(本 commit) + +- `pc-client/src/components/OIMAvatar/index.tsx`:中文名头像取首字(王志远→王,与手机端一致);群头像无真实图时改为 2×2 蓝灰拼格(浅灰底 + 蓝/灰蓝交替),加载失败同样回退拼格。 +- `pc-client/src/styles/global.scss`:新增 `--avatar-gray`、`--search-bg`、`--chat-bg`、`--call-wait-bg` 四个 Token。 +- 收敛近似灰为统一 Token:`#F3F5F7`(聊天区底/hover/搜索框)→ `var(--chat-bg)`;`#F4F5F7`(设置页分隔块)→ `var(--chat-bg)`;`#F2F8FF`(呼叫等待底)→ `var(--call-wait-bg)`。全 `pc-client/src` 无新硬编码色残留。 +- `account-service/server.js`:`reset_password` 重置密码后调用 `forceLogoutAll` 踢掉该员工所有端(与停用/删除一致)。 +- `pc-client/package.json` / `package_electron.json`:`prepare: husky install` 改为 `husky install || true` 容错,非 ASCII/嵌套路径下 `npm install` 不再失败。 +- `pc-client/README.md`:补充 husky 容错说明与新安装包版本。 +- `pc-client/package.json` / `src/config/index.ts`:版本号 `1.0.0 → 1.0.1`,与新的验收安装包对应(旧 `畅联_1.0.0.exe` 作废)。 + +## 5. 已知事项 + +- 双端服务器地址仍烧死为 `192.168.200.11`(mobile `lib/config.dart`、pc `.env`),换服务器 IP 需双端重打包,见质检审核员备注。 +- Electron 22 偏旧 + `webSecurity: false`:内网 HTTP 场景可接受;若开外网必须先升级并开 HTTPS。 +- IM token 明文存本地、登录无失败次数限制、CORS 全开:内网单人管理可接受,开外网时随 HTTPS 一并收紧。 diff --git a/pc-client/README.md b/pc-client/README.md index 35fd700..e101aeb 100644 --- a/pc-client/README.md +++ b/pc-client/README.md @@ -19,6 +19,7 @@ npm run dev - `npm install` 时注意:本仓库用 allow-scripts 白名单管理依赖安装脚本,需批准 `electron` / `esbuild` / `koffi` 等包的 postinstall(package.json 里已配好 `allowScripts` 字段)。 - `npm run dev` 启动 Vite 开发服务器(默认同时拉起 Electron 窗口)。 +- `npm install` 若在中文/嵌套路径下提示 husky 找不到 `.git`:属已知非阻塞问题,`prepare` 已写成 `husky install || true` 容错,git 钩子不可用但不影响安装与构建。 ### 本地 mock 登录服务(联调用) @@ -40,6 +41,8 @@ npm run mock:account npm run build:win # Windows 安装包 ``` +验收安装包为 `release/Base/1.0.1/畅联_1.0.1.exe`(旧版 `畅联_1.0.0.exe` 已作废)。 + 如果只想验证构建不产出安装包:`npm run build && npx electron-builder`(此命令未验证,以 `package_electron.json` 里的脚本为准)。 ## 其他 diff --git a/pc-client/package-lock.json b/pc-client/package-lock.json index 825cdcc..bf38e61 100644 --- a/pc-client/package-lock.json +++ b/pc-client/package-lock.json @@ -1,12 +1,12 @@ { - "name": "OpenCorp-Base", - "version": "1.0.0", + "name": "changlian-pc", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "OpenCorp-Base", - "version": "1.0.0", + "name": "changlian-pc", + "version": "1.0.1", "hasInstallScript": true, "dependencies": { "@ant-design/icons": "^5.1.4", @@ -15023,22 +15023,6 @@ "node": ">=8.10.0" } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", diff --git a/pc-client/package.json b/pc-client/package.json index d5888c4..531315a 100644 --- a/pc-client/package.json +++ b/pc-client/package.json @@ -1,6 +1,6 @@ { "name": "changlian-pc", - "version": "1.0.0", + "version": "1.0.1", "main": "dist-electron/main/index.js", "description": "OpenIM PC Client.", "author": "blooming", @@ -20,7 +20,7 @@ "format": "prettier --write .", "lint": "eslint --ext .js,.jsx,.ts,.tsx src", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx --fix --quiet src", - "prepare": "husky install", + "prepare": "husky install || true", "postinstall": "patch-package" }, "devDependencies": { diff --git a/pc-client/package_electron.json b/pc-client/package_electron.json index a693bdb..bb8dd56 100644 --- a/pc-client/package_electron.json +++ b/pc-client/package_electron.json @@ -26,7 +26,7 @@ "format": "prettier --write .", "lint": "eslint --ext .js,.jsx,.ts,.tsx src", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx --fix --quiet src", - "prepare": "husky install" + "prepare": "husky install || true" }, "engines": { "node": "^14.18.0 || >=16.0.0" diff --git a/pc-client/src/components/OIMAvatar/index.tsx b/pc-client/src/components/OIMAvatar/index.tsx index 9614b5d..443a990 100644 --- a/pc-client/src/components/OIMAvatar/index.tsx +++ b/pc-client/src/components/OIMAvatar/index.tsx @@ -8,12 +8,12 @@ import { avatarList, getDefaultAvatar } from "@/utils/avatar"; const default_avatars = avatarList.map((item) => item.name); -// 头像文字:两个字以内原样显示;中文名取最后一个字,其他取首字母 +// 头像文字:与手机端一致,中文名取首字(王志远→王);非中文取首字母大写 const getDisplayText = (text?: string) => { if (!text) return text; const trimmed = text.trim(); if (trimmed.length <= 2) return trimmed; - if (/^[一-龥]/.test(trimmed)) return trimmed.slice(-1); + if (/^[一-龥]/.test(trimmed)) return trimmed[0]; return trimmed[0].toUpperCase(); }; @@ -62,6 +62,34 @@ const OIMAvatar: React.FC = (props) => { } }; + // 群头像:与手机端一致,无真实头像(或加载失败)时用 2x2 蓝灰拼格(浅灰底 + 蓝/灰蓝交替小方块) + const showGroupGrid = isgroup && (!src || errorHolder === default_group); + if (showGroupGrid) { + const cell = (size - 10) / 2; + const gridColors = ["var(--primary)", "var(--avatar-gray)", "var(--avatar-gray)", "var(--primary)"]; + return ( +
+ {gridColors.map((c, i) => ( +
+ ))} +
+ ); + } + return ( { className={clsx( styles["conversation-item"], "border border-transparent", - isActive ? `bg-[var(--primary-active)]` : "hover:bg-[#F3F5F7]", + isActive ? `bg-[var(--primary-active)]` : "hover:bg-[var(--chat-bg)]", )} onClick={toSpecifiedConversation} > diff --git a/pc-client/src/pages/chat/ConversationSider/index.tsx b/pc-client/src/pages/chat/ConversationSider/index.tsx index f4fe416..d0687e1 100644 --- a/pc-client/src/pages/chat/ConversationSider/index.tsx +++ b/pc-client/src/pages/chat/ConversationSider/index.tsx @@ -152,7 +152,7 @@ const ConversationSider = () => {
{t("placeholder.chat")}
} placeholder={t("placeholder.search")} allowClear diff --git a/pc-client/src/pages/chat/EmptyChat.tsx b/pc-client/src/pages/chat/EmptyChat.tsx index 16dc3bf..50c265d 100644 --- a/pc-client/src/pages/chat/EmptyChat.tsx +++ b/pc-client/src/pages/chat/EmptyChat.tsx @@ -5,7 +5,7 @@ export const EmptyChat = () => { const { t } = useTranslation(); return ( - +
diff --git a/pc-client/src/pages/chat/queryChat/ChatContent.tsx b/pc-client/src/pages/chat/queryChat/ChatContent.tsx index 138fecd..3432d6f 100644 --- a/pc-client/src/pages/chat/queryChat/ChatContent.tsx +++ b/pc-client/src/pages/chat/queryChat/ChatContent.tsx @@ -141,7 +141,7 @@ const ChatContent = () => { return ( {renderBody()} diff --git a/pc-client/src/pages/chat/queryChat/ChatFooter/index.tsx b/pc-client/src/pages/chat/queryChat/ChatFooter/index.tsx index e46c276..62e9f2b 100644 --- a/pc-client/src/pages/chat/queryChat/ChatFooter/index.tsx +++ b/pc-client/src/pages/chat/queryChat/ChatFooter/index.tsx @@ -185,7 +185,7 @@ const ChatFooter: ForwardRefRenderFunction = (_, ref) => { {/* 录音条 */} {recording && ( -
+
{t("placeholder.microphone")} {seconds}″ diff --git a/pc-client/src/pages/chat/queryChat/GroupSetting/GroupSettings.tsx b/pc-client/src/pages/chat/queryChat/GroupSetting/GroupSettings.tsx index 83b3a8e..d02cbe1 100644 --- a/pc-client/src/pages/chat/queryChat/GroupSetting/GroupSettings.tsx +++ b/pc-client/src/pages/chat/queryChat/GroupSetting/GroupSettings.tsx @@ -1,4 +1,4 @@ -import { RightOutlined } from "@ant-design/icons"; +import { RightOutlined } from "@ant-design/icons"; import { Button, Divider, Upload } from "antd"; import clsx from "clsx"; import { t } from "i18next"; @@ -97,7 +97,7 @@ const GroupSettings = ({
- + {currentGroupInfo && isJoinGroup && ( )} - + - +
@@ -131,11 +131,11 @@ const GroupSettings = ({ - + {isOwner && ( <> - + = (
- + - +
{isFriend && ( diff --git a/pc-client/src/pages/common/GroupCardModal/index.tsx b/pc-client/src/pages/common/GroupCardModal/index.tsx index f63b574..810e22d 100644 --- a/pc-client/src/pages/common/GroupCardModal/index.tsx +++ b/pc-client/src/pages/common/GroupCardModal/index.tsx @@ -153,7 +153,7 @@ const GroupCardModal: ForwardRefRenderFunction<
) : ( -
+
{`${t("placeholder.groupMember")}:${ groupData?.memberCount }`}
diff --git a/pc-client/src/pages/common/RtcCallModal/RtcLayout.tsx b/pc-client/src/pages/common/RtcCallModal/RtcLayout.tsx index 28bef0f..90ada9e 100644 --- a/pc-client/src/pages/common/RtcCallModal/RtcLayout.tsx +++ b/pc-client/src/pages/common/RtcCallModal/RtcLayout.tsx @@ -87,7 +87,7 @@ export const RtcLayout = ({
{renderContent()} diff --git a/pc-client/src/pages/contact/ContactSider.tsx b/pc-client/src/pages/contact/ContactSider.tsx index 55b9461..f53f695 100644 --- a/pc-client/src/pages/contact/ContactSider.tsx +++ b/pc-client/src/pages/contact/ContactSider.tsx @@ -91,7 +91,7 @@ const ContactSider = () => { "mx-2 flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm", index === selectIndex ? "bg-[var(--primary-active)]" - : "hover:bg-[#F3F5F7]", + : "hover:bg-[var(--chat-bg)]", )} onClick={() => { setSelectIndex(index); diff --git a/pc-client/src/styles/global.scss b/pc-client/src/styles/global.scss index d07749a..73b6f29 100644 --- a/pc-client/src/styles/global.scss +++ b/pc-client/src/styles/global.scss @@ -11,6 +11,10 @@ --warn-text: #f53f3f; --moment-text: #6085b1; --searchbar-height: 2.5rem; + --avatar-gray: #8593a8; + --search-bg: #f2f3f5; + --chat-bg: #f5f6f8; + --call-wait-bg: #f2f8ff; } @media only screen and (max-width: 600px) {