1. 16 Sep, 2026 7 commits
  2. 15 Sep, 2026 1 commit
  3. 19 Aug, 2026 13 commits
    • chore: 验收测试用例文档不进仓库 · c3cd3541
      qa-tasks.md 与测试用例 xlsx 是交给执行者的临时产出,随迁移一次性使用,
      不需要随代码长期维护,故从仓库移除并加入忽略清单。文件仍保留在本地工作区。
      
      保留上一次提交中的 ~$ 锁文件忽略规则——Office 打开文档时会生成该类临时文件,
      之前已经因此导致过一次提交失败。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • docs: 补充迁移验收的测试用例清单 · f2c4f8db
      为交给 AI 助手执行的验收测试整理任务说明与用例表。
      
      背景:前端已有 57 个 Playwright 用例。对两个后端各跑一遍全量,
      结果完全一致(56 通过、1 失败,且失败的是同一条),说明界面层行为已经对齐;
      那条失败单独跑必过、并发跑必挂,属用例自身问题,与迁移无关。
      
      据此核对覆盖面,发现现有自动化很不均匀:
      - 账号与权限页 0 条,公司档案与公司人员各仅 1 条,总览页 0 条
      - 现有用例全部以超级管理员身份运行,普通角色的界面差异从未验证
      - 企微用例用 page.route 拦截了接口返回,验证的是界面渲染而非台账的真实变化
      - 按输入位数变化的号码搜索规则(3/4/7/11 位四种行为)完全未覆盖
      
      新增文档:
      - docs/qa-tasks.md:执行说明,含不稳定用例的排查方向与三条红线
      - 测试用例 xlsx:74 条用例分 12 组,结果列带下拉选项,
        另附执行说明与覆盖缺口分析两张表
      
      对两处已知缺陷的写法是「记录实际现象、两边表现应一致」而非「应该成功」,
      避免执行者误判为缺陷去改后端——它们已记录在 docs/migration-backlog.md,计划迁移后处理。
      
      顺带把 Office 的 ~$ 锁文件加入忽略清单。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • style: 统一列表首列缩进、筛选项标签与弹窗高度 · a72946fd
      验收过程中发现的三处界面不一致,均与后端迁移无关,单独提交以便区分。
      
      列表首列缩进
      手机号、公司档案、公司人员、企微四个列表共用同一张卡片,其 padding 为 0,
      表格紧贴卡片边缘,首列文字距左边仅 13px,而设备资产与账号权限两页是 33px。
      未给卡片补 padding,因为那会压缩表格可用宽度,列多的页面(企微 11 列)
      可能因此出现横向滚动;改为只给首列的表头与单元格补内边距。
      实测五个列表页现已全部对齐到 33px,表头与数据行同步移动,不会错位。
      
      筛选项标签
      设备资产的三个筛选与公司人员的两个筛选此前只有 placeholder,
      一旦选中值,"这是什么筛选"就看不见了。改为像手机号与企微那样使用 prefix 插槽,
      把筛选项名称固定显示在左侧,选中值显示在右侧。
      
      弹窗高度
      公司档案的新增与编辑弹窗改为按内容撑高,与手机号、设备一致。
      企微与公司人员保持固定高度不变——它们表单项多,固定高度可避免下拉框展开时弹窗跳动。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • docs: 重写 README,补齐两套后端并行期的配置与启动说明 · 6c1fecef
      原 README 停留在「新后端持久层已就绪、业务接口待重构」的阶段,与实际状态脱节。
      
      新增内容:
      - 目录结构与两个后端的端口分工(Java 7690 / NestJS 7691)
      - 两份 .env 的完整字段,并说明 JWT 密钥必须一致——
        不一致会导致切换或回退时所有人被登出
      - 前端如何用环境变量在两个后端之间切换,无需改代码
      - Prisma 只作只读映射、禁止执行 migrate 的约束
      - 契约比对与业务规则验证工具的用法
      - 相关文档索引与当前迁移进度
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • test: 补齐五个业务模块的 76 条单元测试 · 7aa0eb30
      单元测试总数从 25 增至 101,覆盖认证、账号管理、公司档案、公司人员、
      手机号、企微、设备与图片存储八个部分。
      
      企微联动 17 条
      把此前只靠临时脚本验证的四条规则固化进测试套件:复用已有号码、
      自动建档并回填来源、改绑时只清理本账号所建的号、号码未变时完全不动关联。
      另有一条专门断言三个写方法都在事务内执行——同时写两张表的操作若不整体成事,
      失败会留下无人引用的孤儿号码且毫无征兆。
      
      图片存储 9 条
      含路径穿越拒绝(标识来自 URL,不加限制即可读到目录之外的文件)、
      缩略图长边 240 且小图不放大、缺缩略图时按需补生成、清理时原图与缩略图一并删除。
      这类边界正常使用永远不会触发,功能测试发现不了。
      
      其余模块
      公司档案 9 条(删除前的五类引用检查与提示语拼接、空白值存 null)
      公司人员 10 条(在职离职联动、改回在职时清空离职时间、公司显示名简称优先)
      手机号 14 条(按输入位数变化的五种匹配方式、虚拟号码豁免、剥离 +86 前缀)
      设备 17 条(状态枚举、查重带删除时间条件、编号取最大值加一而非数量加一、
      写库失败时只清新图保留原图)
      
      一处测试写错并已修正:纯空格前缀的正确行为是视为未填、回落到默认前缀,
      与 Java 一致,最初的用例错误地期望它被拒绝。
      
      回归:契约比对 31/31、单元测试 101/101 全部通过。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • fix: 登录锁定返回 429 与具体等待时间,补齐其余错误文案 · 69da729e
      用「业务文案覆盖率」做了一次整体核对:提取 Java 侧 service 与 auth 目录下的全部
      59 条业务文案,与 NestJS 实际抛出的文案比对,找出未覆盖项逐条追查。
      这个方法能发现契约对拍覆盖不到的规则——对拍只走典型路径,
      触发条件苛刻的分支(例如连续失败 20 次)不会被跑到。
      
      发现并修复:登录锁定的响应完全不同
        Java    429「登录失败次数过多,账号已被临时锁定,请约 X 小时后重试或联系管理员解锁」
        NestJS  401「账号或密码错误」
      被锁定与密码错误是两回事:使用者必须知道自己被锁了、还要等多久、可以找谁解锁,
      否则只会反复重试。剩余小时向上取整且至少为 1,避免出现「请约 0 小时后重试」。
      已用连续 20 次失败的实测验证两边一致:第 20 次 401,第 21 次 429 且文案逐字相同。
      
      一并补齐
      - 数据库结构与代码不匹配(Prisma P2021/P2022)时返回「数据库字段未同步,
        请完成数据库迁移后重试」,而不是笼统的服务器故障,提示指向真正的原因
      - 图片目录创建失败、图片写入失败、缩略图回写失败三处 IO 异常的文案与 Java 对齐
      
      核对后确认无需处理的 8 条
      「XX 新增失败」五条是 Java 的 insert 计数防御检查,Prisma 写入失败会直接抛异常,
      两边最终都返回 500 通用文案,行为一致;「认证签名密钥未配置」同样走 500 兜底;
      「页面权限无法保存」是 JSON 序列化失败的兜底,Prisma 侧不存在这一环节。
      
      回归:契约比对 31/31、设备流程 15/15、企微规则 10/10、单元测试 25/25 全部通过。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • feat: 迁移设备资产模块,全部 31 个端点契约对齐 · f4dc0834
      新增六个接口(含 multipart 上传与图片读取),设备上传流程 15/15 通过,
      全量契约比对达成 31 项一致、0 项不一致。
      
      图片处理
      缩略图参数逐项对齐:长边 240、保持比例、小图不放大、透明填白、
      输出 JPEG、文件名为「原标识 + .thumb.jpg」。透明区域填白这一条尤其关键——
      Java 用 TYPE_INT_RGB 加白色填充,若照搬默认行为会让透明 PNG 的缩略图整片发黑。
      
      用 8 张定向构造的夹具图做像素级比对,7 张中有 6 张平均色差为 0.00,
      尺寸与格式完全一致。唯一例外是 CMYK JPEG,判定过程与结论记于 CONTRACT-NOTES 第 17 条:
      原图 rgb(40,120,200) 经 Java 处理后偏离 132,经 NestJS 处理后偏离 25,
      是 Java 的 ImageIO 对 CMYK 解码存在已知偏差。这处差异有意保留,
      对齐到 Java 等于把一个色彩错误搬进新系统。
      
      与 Java 的另一处实现差异(有意)
      Java 为绕开 ImageIO 会把大图读两遍的问题,采用「先落盘再解码校验」,失败后再删文件;
      sharp 可直接从内存缓冲区读取元信息,因此改为「先校验再落盘」。
      结果一致(无效图片一律拒绝且不留文件),且天然不会产生需要清理的野文件。
      
      对拍发现并修正
      企微与公司人员列表的公司显示名应「简称优先、无简称才用全称」,
      且只取存活的公司记录。原实现直接取了全称,导致列表显示与 Java 不一致。
      
      新增 scripts/contract/image-compare.mjs 作为长期回归工具。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • feat: 迁移企业微信资产模块,含手机号联动与事务保护 · cdddf83d
      新增九个接口(含四个下拉搜索与号码查重),契约比对 6/6 一致,
      联动规则验证 10/10 通过,事务回滚验证两边行为一致。
      
      四条手机号联动规则
        绑定时号码已在台账   复用,标记 EXISTING
        绑定时号码不在台账   自动建档(EXTERNAL / 来源 WECOM / 来源 id 指向本账号),标记 CREATED
        改绑或删除账号时     只清理「本账号自己建的」号码,且清理前确认无人在用
        号码没变            完全不动关联,避免把 EXISTING 误改成 CREATED 或误删仍在用的号
      
      判断「这条号码是不是本账号建的」需三个条件同时成立:
      number_type = EXTERNAL、source_asset_type = WECOM、source_asset_id = 本账号 id。
      少判一个就会误删他人的号码,而界面上完全看不出异常。
      
      事务保护
      create、update、softDelete 三个方法都同时写企微账号与手机号台账两张表,
      统一用 prisma.$transaction 包住,事务内所有读写一律走事务客户端。
      漏用一处该操作就跑在事务之外,等于没包住,且不会有任何征兆。
      
      验证方式没有停留在读代码:构造一个名称超出字段长度的请求,
      使号码建好之后企微账号插入失败,再回查号码是否仍在台账。
      Java 与 NestJS 均正确回滚,台账没有留下无人引用的孤儿号码。
      
      整体契约比对:26 项一致、5 项待实现(仅剩设备模块)。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • feat: 迁移手机号码管理模块 · a33af22a
      新增五个接口,契约比对 5/5 一致,业务规则对拍 12/12 一致。
      
      按输入位数变化的号码搜索
      这条规则从接口签名完全看不出来,但直接决定搜索是否可用:
        3 位  按号段前缀匹配
        4 位  按尾号匹配
        7 位  前 3 位与后 4 位组合匹配
        其他  精确匹配
      写成统一的模糊匹配看似等价,实际会让「输入 8888 找尾号」退化成乱匹配。
      对拍用真实数据逐条验证了五种输入长度的结果集完全一致。
      
      其余业务规则
      - 卡类型为「虚拟号码」时豁免 ICCID 与实名人的必填校验——虚拟号本无实体卡与实名主体
      - 号码规范化会剥掉 +86 前缀,最终必须是 11 位纯数字
      - 处置状态不填时默认「正常使用」
      - 手动建档固定 number_type = SELF(企微自动录入的号标为 EXTERNAL 以便追溯来源)
      - 删除不做引用检查,与 Java 一致:台账记录可以先于业务资产撤下
      
      对拍发现:「记录不存在」的状态码各模块并不统一
      公司档案与公司人员返回 400,手机号、设备、企微返回 404——
      后三者各自定义了 NotFoundException 与专属异常处理器。这是既有契约不是笔误,
      照搬处理:NestJS 侧新增 NotFoundException(404)与 BusinessException(400)区分。
      完整映射记于 CONTRACT-NOTES 第 16 条,供设备与企微模块实现时对照。
      
      整体契约比对:19 项一致、12 项待实现。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • feat: 迁移公司档案与公司人员模块 · 5b0f2f4e
      新增九个接口,只读端点契约比对 3/3 一致,写路径对拍 14/14 一致。
      
      公司档案
      - 关键词在公司名、简称、统一社会信用代码、地址、联系人、联系方式六个字段上模糊匹配
      - 删除前检查五类资产的引用(公司人员、企微、抖音、域名、商户),
        并把具体引用方拼进提示语。抖音、域名、商户三张表当前无业务模块在写,
        检查仍须保留:这些模块启用后少查一张就会误删仍被引用的档案
      
      公司人员
      - 在职状态仅允许「在职」「离职」,不填默认在职
      - 状态为离职时必须填离职时间;改回在职时离职时间会被清空,
        避免留下「在职却有离职时间」的自相矛盾记录
      - 删除前检查设备、企微、微信、抖音四类资产是否仍把此人记为使用人或运营人
      - 列表一次性批量查出所属公司名,不逐行查询;已删除的公司也取名称,保证历史数据可读
      
      页面权限改为标注加守卫
      Java 是在每个控制器方法体首行手写 permissions.require(...),改为 RequirePermission 标注
      配合全局守卫执行。查询要求 READ,增删改要求 EDIT,与 Java 逐个方法对应。
      
      对拍发现并对齐的一处差异
      Java 的 DTO 上写有 @NotBlank(message = "公司名称不能为空"),但全局异常处理器
      把所有校验失败统一成「提交内容不符合要求,请检查后重试」,DTO 里的文案实际是死代码。
      NestJS 原本在 service 层校验,会返回更具体的文案,反而与线上行为不符。
      已将必填校验移到管道层(先 trim 再判空),文案与 Java 保持一致。
      
      整体契约比对:14 项一致、17 项待实现。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • feat: 契约比对工具,并补齐 NestJS 缺失的安全响应头 · a1702e1c
      scripts/contract/compare.mjs
      对目标服务重放全部端点,与 Java 录制的快照逐字段比对,差异精确到字段路径。
      此前每验证一个模块都要临时写一次比对脚本,现在一条命令即可,
      且比人工核对更细——首次运行就发现了下面这个人工绝对会漏的问题。
      
      两条比对规则,区分对待:
      - 对象字段顺序:排序后比较。消费方按名字取值,顺序无语义;且 Java 的 Map.of
        迭代顺序取决于 JVM 启动时的随机哈希种子,实测重启前后会变,本就不是稳定契约。
      - 数组元素顺序:严格保持。顺序即业务语义(列表排序、分页),
        若一并排序,"本该倒序却写成正序"这类真缺陷会被掩盖,比对就从保障变成掩护。
      
      响应头差异用显式白名单放行,理由写在代码里:
      - content-type:Java 自身也不统一,部分接口带 charset 部分不带,前端无感
      - set-cookie:Java 在已认证 GET 后下发删除 CSRF cookie 的指令,属框架副作用,不予复现
      
      修复:NestJS 缺少 Spring Security 默认的安全响应头
      X-Content-Type-Options、X-Frame-Options、X-XSS-Protection、
      Cache-Control/Pragma/Expires 六个头此前完全缺失。
      这些头由 Spring Security 开箱提供,换框架后不会自动出现,
      缺了功能完全正常、任何人工测试都发现不了,但防点击劫持、防 MIME 嗅探、
      防敏感数据被缓存三道防线同时消失。补齐后认证端点比对 3/3 一致。
      
      当前全量比对:9 项一致、22 项待实现(尚未迁移的五个业务模块,均返回 404)。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • test: 翻译认证与账号管理的 25 条单元测试 · 9742b99a
      对应 Java 侧三个测试类,逐条保留原有断言意图:
      - InMemoryLoginAttemptGuardTest  9 条 -> login-attempt.service.spec.ts
      - AuthServiceTest                9 条 -> auth.service.spec.ts
      - SystemUserAdminServiceTest     7 条 -> system-user.service.spec.ts
      
      这些用例守的是接口对拍看不到的内部行为:
      
      登录防枚举
      账号不存在、已停用、未设密码三种情况都必须真的执行一次密码比对,
      且所有失败路径的耗时补齐到同一下限。从响应上看这三种情况都是同一句
      「账号或密码错误」,只有单元测试能验证内部确实付出了相同代价。
      被锁定的账号还必须在查数据库之前就被拒绝,避免查库耗时泄露账号是否存在。
      
      会话失效
      改动角色、状态或页面权限后,对方令牌里的 auth_version 必须失配从而立即失效;
      提交内容与原值一致时则保留原登录,不无谓地踢人下线。四条用例分别覆盖。
      漏掉自增,权限修改要等对方令牌自然过期(8 小时)才生效,期间界面无任何异常。
      
      两处实现差异(有意,已在测试注释中说明)
      - Java 用 8 线程并发验证计数不丢不重;Node 单线程执行同步方法本无竞态,
        改为验证等价语义:19 次不锁、第 20 次锁,计数既不多算也不少算。
      - bcrypt 是原生模块,其导出属性无法 spyOn,改用模块级 mock。
      
      时间用可控时钟推进,不真实等待 24 小时。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • feat: 迁移认证与账号管理模块,并修复权限异常被吞成 500 的缺陷 · f86e1024
      NestJS 侧新增(backend-nest/src/auth、src/system-user)
      - 登录、登出、当前身份、CSRF 四个认证接口
      - 账号管理六个接口:列表、新建、改角色与权限、重置密码、锁定列表、解除锁定
      - 会话校验含 auth_version 比对,改密码后由数据库触发器自增该值使旧令牌立即失效
      - 登录锁定:20 次失败锁 24 小时,锁定期内继续尝试不延长,窗口外重新计数
      - 页面权限计算:管理员固定全 EDIT,其余角色以库中配置逐项覆盖
      
      登录恒定耗时的实现差异(有意)
      Java 一请求一线程,用 Thread.sleep 补齐无副作用;Node 单线程若同步阻塞会卡死整个进程,
      因此改为异步 bcrypt 加 setTimeout 补齐。实测两边失败路径耗时差异均在 10ms 内,
      成功抹平库中 cost 10 与 cost 12 哈希之间约 168ms 的天然差距。
      
      修复 Java 侧缺陷:Service 层权限异常返回 500
      GlobalExceptionHandler 缺少 AccessDeniedException 的处理,过滤器链之外抛出的权限异常
      落入兜底分支变成 500「服务器处理失败」。实际后果是普通角色打开无权限页面时,
      看到的是服务器错误提示而非「没有页面权限」,会被误判为系统故障。
      补充处理器后返回 403 与具体原因。Java 测试 114/114 仍全绿,契约快照 31/31 无变化。
      
      对拍结果
      - 认证 4 个接口:CSRF Cookie 属性、401/403 文案、登录响应(键排序后)全部一致
      - 账号管理 6 个接口:13 项断言全部一致
      
      其他
      - pagePermissions 的字段顺序不作为契约:Java 用 Map.of 构建,迭代顺序取决于
        JVM 启动时的随机哈希种子,实测重启前后会变;比对时对象键先排序,数组顺序仍严格比对
      - CSRF cookie 在已认证 GET 后被 Spring 删除的行为不予复现,理由记于 CONTRACT-NOTES 第 14 条
      - 新增两项迁移后需求:超管可重置低于自己角色的密码、账号操作审计日志
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
  4. 18 Aug, 2026 2 commits
    • feat: 建立 NestJS 迁移地基与契约基准 · 7ec26d20
      将后端从 Java Spring Boot 迁移至 TypeScript + NestJS 的第一阶段成果。
      本次只新增代码与工具,不改动任何 Java 业务逻辑(行为冻结,保证契约对拍有效)。
      
      契约基准(scripts/contract/)
      - 录制 Java 后端 65 个响应快照,覆盖只读、错误路径、写操作、企微联动规则、
        开发者专属端点、设备图片上传六组场景
      - 归一化抹去自增 id、时间戳、令牌等易变值,保留结构、字段顺序、类型与文案
      - CONTRACT-NOTES.md 汇总 13 条实现要点,作为 NestJS 实现的唯一依据
      - fixtures/ 内含定向构造的疑难图片(CMYK、EXIF 旋转、渐进式、大尺寸),
        覆盖 sharp 与 Java ImageIO 的已知差异点
      
      NestJS 地基(backend-nest/)
      - Prisma 只作只读映射:schema 由 db pull 从现有库反向生成,禁用 migrate
      - 统一响应外壳、全局异常映射,文案与 Java 逐字一致
      - 解决三处 Java/Node 固有差异:BigInt 主键无法 JSON 序列化、
        时间格式带时区与零毫秒、写入时本地时间被当作 UTC 而偏移 8 小时
      
      Java 侧的两处必要改动
      - application.yml 同时尝试两个 .env 路径,兼容 IDEA(工作目录为项目根)
        与命令行(工作目录为 backend/)两种启动方式
      - 端口 7689 改为 7690,vite 代理同步更新;NestJS 迁移期占用 7691
      
      迁移期不做的行为变更记录在 docs/migration-backlog.md。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • Merge branch 'feat/device-image-and-wecom-crud' into 'master' · 90002afb
      feat: add company people management
      
      See merge request !14
      DaiJiezhang authored
  5. 14 Aug, 2026 1 commit
  6. 12 Aug, 2026 5 commits
  7. 06 Aug, 2026 7 commits
    • Merge branch 'feat/device-image-and-wecom-crud' into 'master' · 870deb9d
      feat: 设备图片性能优化、企业微信资产增删改与可清空字段修复
      
      See merge request !11
      DaiJiezhang authored
    • chore: 把不含密钥的 application.yml 纳入版本管理 · ea21e468
      这个文件此前被 .gitignore 挡在仓库外,导致新克隆的人缺后端配置、服务起不来:
      端口、MyBatis 设置、会话时长、登录失败最小耗时等都只存在于各自本地。
      
      挡住它并没有安全上的必要——数据库账号密码和 JWT 密钥在文件里全是 ${} 环境
      变量占位符,真值放在 .env,而 .env 依然被忽略。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • chore: 统一「手机号资产」文案为「手机号码管理」 · 382ec917
      纯文案替换,不含任何逻辑改动,单独成一次提交,避免 34 个文件的一行改动把
      前面几次提交里真正的逻辑变更淹掉。
      
      范围:后端提示语与注释、前端页面文案与测试断言、openspec 变更文档。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • refactor: 列表页去掉重复的标题栏,菜单文案对齐页面标题 · af4adb03
      三个列表页顶部的「资产列表 共 N 条」整块去掉,与设备资产管理页保持一致。
      总条数底部分页器已经在报,重复一遍只是白占一行高度。
      
      侧边栏「企微资料」改为「企业微信资产」,与页面标题、新增按钮、弹窗标题统一。
      其余菜单项已逐个核对与各页 h2 标题一致,只有这一处不对齐。
      
      手机号码管理页支持从地址栏读取 phoneNumber 预筛选,供企业微信资产页的注册
      手机号链接跳转使用。
      
      本次提交同时包含手机号码管理页文案由「手机号资产」改为「手机号码管理」的
      部分改动——该页的文案替换与上述结构调整改在同一批代码行上,无法干净拆分。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • feat: 企业微信资产支持编辑与删除,并新增关联设备字段 · 83ab85f1
      企微此前只能新增。补上编辑和删除后,难点在注册手机号的联动——新增时填一个
      库里没有的号码,系统会自动建一条外部号码资产(phoneLinkMode=CREATED)并把它
      的 sourceAssetId 指回这条企微。
      
      换号和删除时,那条自动新建的号码要满足三个条件才会被一并软删:
      - phoneLinkMode 是 CREATED(复用的已有号码是独立资产,绝不能碰)
      - 来源标记对得上,防止误删同号但另有出处的记录
      - 没有别的企微、微信、抖音仍在引用它
      
      三道闸门缺一不可。号码没改动时整段联动直接跳过,避免把复用号码误标成新建,
      或把还在用的号码当成旧号清理掉。删除确认框会明说自动新建的号码将一并删除。
      
      关联设备:表单加远程搜索选择,新增和编辑都支持,列表也展示。打开弹窗即预载
      一页设备,否则用户点开下拉只看到「无数据」,得先猜着打字才有列表。已选设备
      不在后端返回的前 20 条里时会被补回列表头部,避免选择框丢掉名称回落成裸 ID。
      
      顺带修复 PhoneAssetValidationException 没有任何 handler 的问题:它会一路落进
      GlobalExceptionHandler 的 Exception 兜底,用户填错手机号却收到「服务器处理
      失败,请稍后重试」。同样地,接口不存在或请求方式不对此前也被兜底成 500,把
      「前后端版本不同步」伪装成线上故障,排查时会往完全错误的方向查;现在分别返回
      400、405、404。
      
      列表信息密度:去掉资产 ID 列与关联字段的(ID:n)后缀,别名收进名称下方做
      浅灰小字,性别做成名称右侧图标,实名状态做成实名人右上角圆点,关联方式做成
      手机号右上角一字角标。注册手机号和关联设备做成链接,跳转到对应资产页并预筛
      出该条——为此手机号码管理页和设备资产管理页补上了读取地址栏参数的能力,
      否则链接点过去只是一个没筛选的全量列表。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • feat: 设备资产图片改用缩略图,并支持按顺序生成设备编号 · ceb45728
      图片性能:列表里 40px 的小图原本加载的是完整原图。实测单张 1672x941 的 PNG
      约 1.8MB,一页 20 条最多 40 张,需要传输约 72MB、浏览器解码后常驻内存约
      250MB;每页条数还能选到 50,翻倍到 180MB / 630MB。图片接口又没有任何缓存头,
      每次刷新、翻页回来都要重下一遍。
      
      改法分四层:
      - 保存时用 ImageReader 降采样解码,一次完成格式校验和长边 240px 缩略图生成。
        实测 1890KB -> 4KB,耗时 82ms,比原先只做校验的全图解码(119ms)还快。
        缩略图先写临时文件再原子替换,并发补生成不会读到半截文件。
      - 列表和弹窗预览改用缩略图,只有点开大图才请求原图;历史图片首次被请求时
        自动补生成一次并落盘。
      - 图片接口加 Cache-Control: max-age=1年, private, immutable。文件名是随机
        UUID、内容永不改写,换图必然换标识,所以缓存安全;用 private 而非 public,
        避免受登录态保护的图片被共享代理缓存后发给别人。
      - 列表图加 loading=lazy、悬停时预取并解码原图,点开大图几乎无等待。
      
      上传交互:上传控件此前没有 accept 属性,系统文件框要枚举目录下所有类型的
      文件;预览又把原图直接塞进 104x78 的框,浏览器同步解码整张图会明显卡顿。
      现在限定图片类型,并用 createImageBitmap 异步解码后缩到 320px 再显示,
      上传的仍是未经处理的原始文件。
      
      设备编号:设备名称输入框加一键生成下一个「前缀N号机」。编号由后端在全库范围
      计算,不按当前页推算——一页只有 20 条,最大编号很可能在别的页上,按当前页
      算会撞上已存在的名字被唯一性校验打回。前缀限定 1-20 位中英文数字,因为它要
      拼进 LIKE 条件,% 和 _ 会被当通配符。
      
      列表同时去掉编号列、图片挪到首列并支持展示两张、无图与加载失败统一显示占位框、
      更新时间只保留日期。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • fix: 让可清空字段的 null 真正写进 UPDATE 语句 · d8736010
      MyBatis-Plus 的 updateById 默认用 FieldStrategy.NOT_NULL,实体里值为 null 的
      字段会被整个从 SET 子句里剔除。表现极具欺骗性:接口返回成功、update_time 也
      更新了,唯独那一列的旧值原封不动,刷新后又冒出来。
      
      已确认受影响的操作:
      - 设备资产「移除图片」保存后列表仍显示旧图
      - 设备资产、手机号码管理「清空使用人 / 运营商 / ICCID / 实名人 / 管理模式 /
        关联设备」保存后均不生效
      
      给这些业务上允许清空的字段声明 updateStrategy = ALWAYS。企微、微信、抖音的
      device_id 目前还没有编辑接口,一并声明是提前立规矩,避免补编辑功能时重蹈覆辙。
      
      没有改全局 update-strategy 配置:那会让所有模块的 update 都把 null 写进库,
      可能在别处造成数据丢失。放开 ALWAYS 的前提是所有 updateById 调用点都走
      「先查全量实体再改字段」,已逐个核对确认。
      
      新增 NullableColumnUpdateTest 直接检查 MyBatis-Plus 生成的 SET 子句,而不是
      mock 掉 Mapper——原来的 Service 测试正是因为 mock 了 Mapper、SQL 从未被验证,
      才让这个缺陷漏了出去。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
  8. 05 Aug, 2026 4 commits
    • Merge branch 'codex/merge-unmerged-auth-commits' into 'master' · fc0e6702
      feat: 手机号资产按设备名称关联并统一列表与弹窗字段
      
      See merge request !10
      DaiJiezhang authored
    • feat: 手机号资产按设备名称关联并统一列表与弹窗字段 · 13400512
      关联设备原本只显示和输入设备 ID,用户得自己去设备资产页记数字。
      改为按设备名称搜索选择,列表也直接显示名称。
      
      - 新增 GET /api/phone-assets/lookups/devices,走手机号页 READ 权限而不是设备页的
        管理员权限,避免只有手机号权限的用户搜不到设备
      - PhoneAssetResponse 增加 deviceName,分页时一次 IN 查询批量解析,不逐行查库
      - 弹窗设备 ID 输入框改为可搜索下拉,编辑时预置当前设备保证名称回显
      - 弹窗字段名统一为列表叫法:卡类型→运营商、实名归属→实名人、管理方式→管理模式、
        处置状态→使用状态、设备 ID→关联设备
      - 运营商增加「电销卡「联通」」
      - 列表与弹窗字段顺序调整为 手机号、实名人、运营商
      - ICCID 不再单独占列,收进手机号单元格由图标点击弹出,图标用内联 SVG 以免引入
        不在 package.json 里的 @element-plus/icons-vue
      - 修复下拉已选中的值被 placeholder 灰色规则一起染灰
      - 修复筛选框写死宽度导致手机号提示语截断、重置按钮换行
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored
    • Merge branch 'codex/merge-unmerged-auth-commits' into 'master' · 3d2fe0a8
      列表分页器统一与界面文案中文化
      
      See merge request !9
      DaiJiezhang authored
    • style: 移除全站副标题并清理 eyebrow 样式 · 10a82a20
      删除侧边栏品牌副标题、账号权限页与历史参考页副标题,
      连同登录页早已停用的 login-brand__eyebrow 一并清理,
      eyebrow 这个类自此在源码中不再出现。
      
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
      DaiJiezhang authored