Files
STP/docs/architecture/详细设计/供应商信息维护模块.md
T
weiwen_ye 3bd2a0a31d refactor(design): 统一供应商信息维护模块右侧浮窗为小滨AI助手样式
- 移除原“安全状态”和“文件管理说明”静态浮窗
- 新增小滨AI助手对话气泡组件,整合安全提醒、安全状态及文件管理说明
- 调整右侧助手位置,使整体布局更协调
- 优化视觉风格,保持与平台品牌色一致
2026-07-01 18:25:43 +08:00

57 KiB
Raw Blame History

供应商信息维护模块

文档名称 供应商信息维护模块详细设计
文档版本 V1.0
编制日期 2026-07-01
关联文档 供应商注册模块登录模块》《SRM 与 STP 数据交换接口方案说明书

1. 模块概述

1.1 建设目标

供应商成功注册并登录后,需具备自主维护企业信息的能力。本模块覆盖密码修改、联系人信息变更、公司证明文件(营业执照)更新三大高频维护场景,确保账户安全、业务通知可达、企业资质动态更新,所有变更实时同步至SRM并全量审计。

1.2 设计原则

原则 说明
安全优先 修改密码需验证旧密码;修改核心联系方式需双因子验证;敏感操作记录哈希链审计。
平滑同步 信息变更后实时同步SRM,确保两平台数据一致(最终一致性)。
版本可溯 营业执照等文件更新时保留历史版本,支持追溯。
自助为主,人工兜底 90%场景供应商自助完成,旧手机不可用时提供线下人工变更通道。

1.3 适用角色与场景

角色 场景
已登录供应商 修改密码、更新联系人、重新上传营业执照。
供应商管理专员 查看变更记录、处理异常变更申请(旧联系方式不可用时)。
系统管理员 配置安全策略(密码复杂度、会话有效期)。

2. 页面UI详细设计(工作台端)

2.1 “企业信息”总览页

2.1.1 “基本资料”Tab

(一)页面 UI 设计

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 800" font-family="'Microsoft YaHei', sans-serif">
  <defs>
    <filter id="shadowLight" x="-2%" y="-2%" width="104%" height="104%">
      <feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.04"/>
    </filter>
    <linearGradient id="brandGrad" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#c2185b"/><stop offset="100%" stop-color="#e91e63"/>
    </linearGradient>
  </defs>

  <rect width="1100" height="800" fill="#f5f7fa"/>
  
  <!-- 主卡片 -->
  <rect x="60" y="30" width="980" height="740" rx="16" fill="#fff" filter="url(#shadowLight)"/>
  
  <!-- 页面标题 -->
  <text x="100" y="75" font-size="22" font-weight="bold" fill="#1a2332">企业信息管理</text>
  <text x="100" y="102" font-size="14" fill="#8895aa">管理您的企业资料、安全凭证和资质文件</text>
  <line x1="100" y1="125" x2="1000" y2="125" stroke="#f0f2f7" stroke-width="2"/>

  <!-- ===== Tab 切换栏(三个) ===== -->
  <!-- Tab1: 基本资料(激活状态) -->
  <rect x="100" y="145" width="120" height="40" rx="8" fill="#c2185b"/>
  <text x="160" y="171" text-anchor="middle" font-size="14" font-weight="bold" fill="#fff">基本资料</text>
  
  <!-- Tab2: 安全设置 -->
  <rect x="230" y="145" width="120" height="40" rx="8" fill="#f5f5f5"/>
  <text x="290" y="171" text-anchor="middle" font-size="14" fill="#666">安全设置</text>
  
  <!-- Tab3: 资质文件 -->
  <rect x="360" y="145" width="120" height="40" rx="8" fill="#f5f5f5"/>
  <text x="420" y="171" text-anchor="middle" font-size="14" fill="#666">资质文件</text>

  <!-- ============================================================ -->
  <!-- ===== 当前展示:基本资料 Tab 内容(默认激活) ===== -->
  <!-- ============================================================ -->
  
  <!-- 企业基本信息展示区 -->
  <text x="140" y="215" font-size="15" font-weight="bold" fill="#4a5568">📋 企业基本信息</text>
  
  <text x="140" y="248" font-size="14" fill="#888">企业全称:</text>
  <text x="250" y="248" font-size="14" fill="#1a2332" font-weight="bold">滨化新材料有限公司</text>
  
  <text x="140" y="278" font-size="14" fill="#888">统一社会信用代码:</text>
  <text x="300" y="278" font-size="14" fill="#1a2332">91370100MA3XXXXXXX</text>
  
  <text x="140" y="308" font-size="14" fill="#888">注册资本:</text>
  <text x="250" y="308" font-size="14" fill="#1a2332">5,000.00 万元</text>
  <text x="440" y="308" font-size="14" fill="#888">法定代表人:</text>
  <text x="550" y="308" font-size="14" fill="#1a2332">张伟</text>
  
  <text x="140" y="338" font-size="14" fill="#888">行业分类:</text>
  <text x="250" y="338" font-size="14" fill="#1a2332">C2614 有机化学原料制造</text>
  
  <text x="140" y="368" font-size="14" fill="#888">注册地址:</text>
  <text x="250" y="368" font-size="14" fill="#1a2332">山东省滨州市黄河五路888号</text>

  <!-- 联系人信息区 -->
  <line x1="140" y1="400" x2="1000" y2="400" stroke="#f0f2f7" stroke-width="1"/>
  <text x="140" y="430" font-size="15" font-weight="bold" fill="#4a5568">📞 联系人信息</text>
  
  <text x="140" y="462" font-size="14" fill="#888">联系人姓名:</text>
  <text x="250" y="462" font-size="14" fill="#1a2332" font-weight="bold">李强</text>

  <text x="140" y="498" font-size="14" fill="#888">手机号码:</text>
  <text x="250" y="498" font-size="14" fill="#1a2332">138****8001</text>
  <text x="250" y="518" font-size="12" fill="#4caf50">✅ 已验证</text>

  <text x="140" y="550" font-size="14" fill="#888">电子邮箱:</text>
  <text x="250" y="550" font-size="14" fill="#1a2332">liqiang@example.com</text>
  <text x="250" y="570" font-size="12" fill="#4caf50">✅ 已验证</text>
  
  <rect x="145" y="585" width="80" height="28" rx="6" fill="#e3f2fd" stroke="#1565c0"/>
  <text x="185" y="605" text-anchor="middle" font-size="12" fill="#1565c0">变更联系人</text>

  <!-- 资料更新时间 -->
  <text x="140" y="645" font-size="12" fill="#b0bbcc">资料最后更新:2026-06-30 14:30</text>
  <text x="380" y="645" font-size="12" fill="#b0bbcc">| 供应商编号:SUP_20260630_0158</text>
  
  <!-- ============================================================ -->
  <!-- ===== 右侧:小滨AI助手提醒(替换原静态浮窗) ===== -->
  <!-- ============================================================ -->
  <rect x="720" y="200" width="240" height="195" rx="10" fill="#ffffff" stroke="#e0e4ec" filter="url(#shadowLight)"/>
  
  <!-- 头像 + 标题 -->
  <circle cx="745" cy="228" r="16" fill="url(#brandGrad)"/>
  <text x="745" y="233" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">小滨</text>
  <text x="770" y="233" font-size="14" font-weight="bold" fill="#1a2332">AI 助手</text>
  
  <!-- 小滨对话气泡 -->
  <rect x="725" y="255" width="220" height="125" rx="8" fill="#f0f4ff" stroke="#d1e3ff"/>
  <path d="M740 255 L748 245 L756 255 Z" fill="#f0f4ff" stroke="#d1e3ff" stroke-width="1"/>
  
  <text x="740" y="278" font-size="12" fill="#1565c0" font-weight="bold">💡 安全提醒</text>
  <text x="740" y="300" font-size="11" fill="#4a5568">• 修改手机号需验证新旧号码</text>
  <text x="740" y="320" font-size="11" fill="#4a5568">• 修改密码需验证当前密码</text>
  <text x="740" y="340" font-size="11" fill="#4a5568">• 所有变更将同步至SRM系统</text>
  <text x="740" y="360" font-size="11" fill="#4a5568">• 操作记录将保存7年</text>
</svg>

(二)“变更联系人”完整处理流程

业务语义:此操作表示企业联系人已更换(如人员离职、调岗),需要将姓名、手机、邮箱作为一个整体替换为新联系人的信息,而非逐项零散修改。

1. 整体流程泳道图

flowchart TD
    subgraph 供应商端
        A[点击“变更联系人”] --> B[弹出变更向导(Step 1/3]
        B --> C[Step 1:验证当前联系人]
        C --> D[选择验证方式:手机或邮箱]
        D --> E[发送验证码至选定方式]
        E --> F[输入验证码]
        F --> G{验证通过?}
        G -->|失败| H[提示错误,可重试]
        G -->|成功| I[进入 Step 2]
        
        I --> J[Step 2:填写新联系人信息]
        J --> K[填写新姓名、新手机、新邮箱]
        K --> L[点击“下一步”]
        L --> M[后端校验:新手机/邮箱全局唯一]
        M -->|已被占用| N[提示具体冲突项]
        M -->|可用| O[进入 Step 3]
        
        O --> P[Step 3:验证新联系方式]
        P --> Q[分别发送验证码至新手机和新邮箱]
        Q --> R[输入手机验证码 + 邮箱验证码]
        R --> S[点击“确认变更”]
        S --> T[提交全部变更]
    end

    subgraph STP后端
        STP1[接收变更请求] --> STP2[校验旧联系人验证Token]
        STP2 --> STP3[校验新手机验证Token]
        STP3 --> STP4[校验新邮箱验证Token]
        STP4 --> STP5[原子性更新三个字段]
        STP5 --> STP6[更新 login_name(若登录名为手机号)]
        STP6 --> STP7[调用SRM批量更新联系人接口]
        STP7 --> STP8[记录审计日志(含新旧完整数据)]
        STP8 --> STP9[发送通知至新旧联系人]
        STP9 --> STP10[返回成功]
    end

    subgraph 外部系统
        SRM[SRM系统]
        NOTIFY[通知服务]
    end

    STP7 --> SRM
    STP9 --> NOTIFY

2. 详细交互时序图

sequenceDiagram
    participant U as 供应商
    participant FE as 前端
    participant Svc as 供应商服务
    participant Cache as Redis
    participant SMS as 短信网关
    participant Email as 邮件网关
    participant DB as 数据库
    participant SRM as SRM系统

    Note over U,SRM: === Step 1: 验证旧联系人 ===
    U->>FE: 点击“变更联系人”
    FE-->>U: 弹出第一步验证窗
    U->>FE: 选择验证方式(手机/邮箱)
    FE->>Svc: POST /api/v1/auth/verify/send { type:PHONE/EMAIL }
    Svc->>SMS/Email: 发送验证码
    Svc->>Cache: SET verify:old:{userId} TTL=300
    U->>FE: 输入验证码
    FE->>Svc: POST /api/v1/auth/verify/check { code }
    Svc->>Cache: 比对
    Svc-->>FE: { oldOtpToken: "temp_token_old" }

    Note over U,SRM: === Step 2: 填写新联系人 ===
    U->>FE: 填写新姓名、新手机、新邮箱
    FE->>Svc: 前端校验格式
    U->>FE: 点击“下一步”
    FE->>Svc: POST /api/v1/supplier/contact/check-unique { newPhone, newEmail }
    Svc->>DB: 查询新手机/邮箱是否已被占用
    alt 已被占用
        Svc-->>FE: 409 冲突,提示具体占用项
    else 可用
        Svc-->>FE: 200 可用,进入Step 3
    end

    Note over U,SRM: === Step 3: 验证新联系方式 ===
    FE->>Svc: POST /api/v1/auth/verify/send { type:PHONE, target:新手机, purpose:NEW }
    Svc->>SMS: 发送验证码
    Svc->>Cache: SET verify:new_phone:{userId} TTL=300
    
    FE->>Svc: POST /api/v1/auth/verify/send { type:EMAIL, target:新邮箱, purpose:NEW }
    Svc->>Email: 发送验证码
    Svc->>Cache: SET verify:new_email:{userId} TTL=300
    
    U->>FE: 输入两个验证码
    FE->>Svc: POST /api/v1/auth/verify/check { type:PHONE, code }
    FE->>Svc: POST /api/v1/auth/verify/check { type:EMAIL, code }
    Svc-->>FE: { newPhoneToken, newEmailToken }

    Note over U,SRM: === 最终提交 ===
    U->>FE: 点击“确认变更”
    FE->>Svc: PUT /api/v1/supplier/contact/change-all
    
    Svc->>Svc: 校验三个Token有效性(old/newPhone/newEmail
    Svc->>DB: 开启事务
    Svc->>DB: UPDATE contact_name, contact_phone, contact_email
    Svc->>DB: UPDATE login_name(若登录名=手机号)
    Svc->>DB: 提交事务
    
    Svc->>SRM: POST /api/v1/supplier/contact/batch-update { contactName, phone, email }
    SRM-->>Svc: 200 OK
    
    Svc->>Cache: DEL 所有验证码缓存
    Svc->>DB: INSERT 审计日志(含完整新旧数据)
    Svc->>SMS: 发送通知至旧手机(告知已变更)
    Svc->>Email: 发送通知至旧邮箱
    Svc->>SMS: 发送通知至新手机(告知已绑定)
    Svc->>Email: 发送通知至新邮箱
    
    Svc-->>FE: 200 成功
    FE-->>U: 变更成功,页面刷新

3. 关键业务规则

规则项 说明
原子性 姓名、手机、邮箱必须在同一个数据库事务中同时更新,不允许部分成功。
唯一性校验 新手机号和新邮箱必须全局唯一(未被其他ACTIVE供应商占用),与注册规则一致。
登录名联动 若系统使用手机号作为登录名(login_name),更换手机号后login_name同步更新,旧手机号不可再用于登录。
双因子验证 必须同时验证三个Token(旧联系人、新手机、新邮箱),任一缺失或失效则拒绝变更。
Token有效期 所有验证Token有效期为5分钟,超时需重新获取。
操作频率限制 同一供应商30天内最多变更联系人3次
SRM同步 调用SRM的批量更新接口一次性传递三个字段,确保SRM侧原子更新。
会话处理 变更手机号后,不强制踢出当前登录会话(旧会话仍有效),但密码找回等使用手机号的功能将指向新号。
审计粒度 审计日志需记录变更前的完整联系人信息和变更后的完整信息。

4. 异常场景处理

异常场景 用户侧表现 后端处理
新手机号已被占用 提示“该手机号已被其他供应商绑定” 返回冲突字段,阻塞提交
新邮箱已被占用 提示“该邮箱已被其他供应商绑定” 返回冲突字段,阻塞提交
旧联系人验证超时(>5min 提示“验证已过期,请重新获取” 清除旧Token,要求重新验证
新手机验证超时 提示“手机验证码已过期” 清除该Token,要求重发
新邮箱验证超时 提示“邮箱验证码已过期” 清除该Token,要求重发
SRM同步失败 用户无感知(STP侧先成功) 异步重试5次,仍失败告警管理员
30天内变更超限 提示“30天内仅允许变更联系人3次” 返回限流错误码

5. 接口规范

最终提交接口

项目 内容
路径 /api/v1/supplier/contact/change-all
方法 PUT
认证 需要登录态(JWT

请求体

{
  "newContactName": "王芳",
  "newPhone": "13900139000",
  "newEmail": "wangfang@example.com",
  "oldOtpToken": "eyJ...",
  "newPhoneOtpToken": "eyJ...",
  "newEmailOtpToken": "eyJ..."
}

成功响应

{
  "code": "0",
  "message": "联系人变更成功",
  "data": {
    "contactName": "王芳",
    "contactPhone": "13900139000",
    "contactEmail": "wangfang@example.com"
  }
}

2.1.2 Tab 2:安全设置

(一)页面 UI 设计(切换到“安全设置”Tab 时的展示)

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 920" font-family="'Microsoft YaHei', sans-serif">
  <defs>
    <filter id="shadowLight" x="-2%" y="-2%" width="104%" height="104%">
      <feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.04"/>
    </filter>
    <linearGradient id="brandGrad" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#c2185b"/><stop offset="100%" stop-color="#e91e63"/>
    </linearGradient>
    <linearGradient id="statusGreen" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#4caf50"/><stop offset="100%" stop-color="#66bb6a"/>
    </linearGradient>
    <linearGradient id="statusRed" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#f44336"/><stop offset="100%" stop-color="#ef5350"/>
    </linearGradient>
  </defs>

  <rect width="1100" height="920" fill="#f5f7fa"/>
  <rect x="60" y="30" width="980" height="860" rx="16" fill="#fff" filter="url(#shadowLight)"/>
  
  <!-- 页面标题 -->
  <text x="100" y="75" font-size="22" font-weight="bold" fill="#1a2332">企业信息管理</text>
  <text x="100" y="102" font-size="14" fill="#8895aa">管理您的企业资料、安全凭证和资质文件</text>
  <line x1="100" y1="125" x2="1000" y2="125" stroke="#f0f2f7" stroke-width="2"/>

  <!-- ===== Tab 切换栏(安全设置高亮) ===== -->
  <rect x="100" y="145" width="120" height="40" rx="8" fill="#f5f5f5"/>
  <text x="160" y="171" text-anchor="middle" font-size="14" fill="#666">基本资料</text>
  
  <rect x="230" y="145" width="120" height="40" rx="8" fill="#c2185b"/>
  <text x="290" y="171" text-anchor="middle" font-size="14" font-weight="bold" fill="#fff">安全设置</text>
  
  <rect x="360" y="145" width="120" height="40" rx="8" fill="#f5f5f5"/>
  <text x="420" y="171" text-anchor="middle" font-size="14" fill="#666">资质文件</text>

  <!-- ============================================================ -->
  <!-- ===== 安全设置 Tab 内容 ===== -->
  <!-- ============================================================ -->
  
  <text x="140" y="215" font-size="15" font-weight="bold" fill="#4a5568">🔐 账户安全设置</text>
  <text x="140" y="242" font-size="13" fill="#8895aa">维护您的登录凭证,查看登录记录,确保账户安全</text>

  <!-- 1. 登录密码 -->
  <rect x="140" y="270" width="820" height="60" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
  <text x="170" y="298" font-size="14" font-weight="bold" fill="#1a2332">登录密码</text>
  <text x="170" y="318" font-size="12" fill="#8895aa">最后修改时间:2026-06-01 09:30</text>
  <text x="170" y="336" font-size="12" fill="#4caf50">密码强度:强</text>
  <rect x="770" y="278" width="160" height="34" rx="6" fill="#c2185b"/>
  <text x="850" y="300" text-anchor="middle" font-size="13" font-weight="bold" fill="#fff">修改密码</text>

  <!-- 2. 绑定手机号 -->
  <rect x="140" y="350" width="820" height="60" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
  <text x="170" y="378" font-size="14" font-weight="bold" fill="#1a2332">绑定手机号</text>
  <text x="170" y="398" font-size="12" fill="#4caf50">✅ 已验证:138****8001</text>
  <rect x="770" y="358" width="160" height="34" rx="6" fill="#1565c0"/>
  <text x="850" y="380" text-anchor="middle" font-size="13" font-weight="bold" fill="#fff">更换手机号</text>

  <!-- 3. 绑定邮箱 -->
  <rect x="140" y="430" width="820" height="60" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
  <text x="170" y="458" font-size="14" font-weight="bold" fill="#1a2332">绑定邮箱</text>
  <text x="170" y="478" font-size="12" fill="#4caf50">✅ 已验证:liqiang@example.com</text>
  <rect x="770" y="438" width="160" height="34" rx="6" fill="#1565c0"/>
  <text x="850" y="460" text-anchor="middle" font-size="13" font-weight="bold" fill="#fff">更换邮箱</text>

  <!-- 4. 当前登录设备管理 -->
  <rect x="140" y="510" width="820" height="100" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
  <text x="170" y="538" font-size="14" font-weight="bold" fill="#1a2332">当前登录设备</text>
  <text x="170" y="562" font-size="12" fill="#8895aa">当前有 2 台设备保持登录状态</text>
  <text x="170" y="578" font-size="12" fill="#1565c0">▶ 设备 1Chrome/Windows · IP 192.168.1.100 · 2026-07-01 09:30(当前设备)</text>
  <text x="170" y="590" font-size="12" fill="#666">▶ 设备 2Safari/iPhone · IP 10.0.0.50 · 2026-06-30 22:15</text>
  
  <!-- 操作按钮 -->
  <rect x="770" y="518" width="160" height="34" rx="6" fill="#ff6f00"/>
  <text x="850" y="540" text-anchor="middle" font-size="13" font-weight="bold" fill="#fff">踢出所有设备</text>
  <text x="170" y="610" font-size="12" fill="#888">💡 点击“踢出所有设备”将强制所有设备(包括当前设备)重新登录</text>

  <!-- ===== 5. 登录日志查看(新增区域) ===== -->
  <line x1="140" y1="635" x2="1000" y2="635" stroke="#f0f2f7" stroke-width="1"/>
  
  <!-- 日志区域标题+操作栏 -->
  <text x="140" y="665" font-size="15" font-weight="bold" fill="#4a5568">📋 登录日志</text>
  <text x="140" y="690" font-size="13" fill="#8895aa">查看最近 90 天的登录记录,如发现异常登录请及时修改密码</text>
  
  <rect x="770" y="650" width="160" height="34" rx="6" fill="#e0e4ec"/>
  <text x="850" y="672" text-anchor="middle" font-size="13" fill="#666">刷新日志</text>
  <rect x="940" y="650" width="100" height="34" rx="6" fill="#c2185b"/>
  <text x="990" y="672" text-anchor="middle" font-size="13" font-weight="bold" fill="#fff">查看全部</text>

  <!-- 日志表格 -->
  <rect x="140" y="710" width="820" height="180" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
  
  <!-- 表头 -->
  <rect x="140" y="710" width="820" height="36" rx="8" fill="#f0f2f7"/>
  <text x="170" y="733" font-size="13" font-weight="bold" fill="#4a5568">登录时间</text>
  <text x="330" y="733" font-size="13" font-weight="bold" fill="#4a5568">IP 地址</text>
  <text x="470" y="733" font-size="13" font-weight="bold" fill="#4a5568">地理位置</text>
  <text x="610" y="733" font-size="13" font-weight="bold" fill="#4a5568">设备/浏览器</text>
  <text x="800" y="733" font-size="13" font-weight="bold" fill="#4a5568">状态</text>

  <!-- 行1:当前设备 -->
  <rect x="150" y="755" width="800" height="30" rx="4" fill="#e8f5e9"/>
  <text x="170" y="775" font-size="13" fill="#1a2332">2026-07-01 09:30:25</text>
  <text x="330" y="775" font-size="13" fill="#1a2332">192.168.1.100</text>
  <text x="470" y="775" font-size="13" fill="#1a2332">山东·滨州</text>
  <text x="610" y="775" font-size="13" fill="#1a2332">Chrome/Windows</text>
  <rect x="790" y="763" width="60" height="16" rx="3" fill="url(#statusGreen)"/>
  <text x="820" y="775" text-anchor="middle" font-size="11" fill="#fff">当前设备</text>

  <!-- 行2:历史登录 -->
  <rect x="150" y="790" width="800" height="30" rx="4" fill="#fff"/>
  <text x="170" y="810" font-size="13" fill="#1a2332">2026-06-30 22:15:03</text>
  <text x="330" y="810" font-size="13" fill="#1a2332">10.0.0.50</text>
  <text x="470" y="810" font-size="13" fill="#1a2332">山东·滨州</text>
  <text x="610" y="810" font-size="13" fill="#1a2332">Safari/iOS</text>
  <rect x="790" y="798" width="60" height="16" rx="3" fill="url(#statusGreen)"/>
  <text x="820" y="810" text-anchor="middle" font-size="11" fill="#fff">正常</text>

  <!-- 行3:异常登录预警示例 -->
  <rect x="150" y="825" width="800" height="30" rx="4" fill="#ffebee"/>
  <text x="170" y="845" font-size="13" fill="#c62828">2026-06-29 03:45:12</text>
  <text x="330" y="845" font-size="13" fill="#c62828">203.0.113.45</text>
  <text x="470" y="845" font-size="13" fill="#c62828">境外·未知地区</text>
  <text x="610" y="845" font-size="13" fill="#c62828">Firefox/Linux</text>
  <rect x="790" y="833" width="60" height="16" rx="3" fill="url(#statusRed)"/>
  <text x="820" y="845" text-anchor="middle" font-size="11" fill="#fff">异常</text>

  <!-- 行4 -->
  <rect x="150" y="860" width="800" height="30" rx="4" fill="#fff"/>
  <text x="170" y="880" font-size="13" fill="#1a2332">2026-06-28 18:00:01</text>
  <text x="330" y="880" font-size="13" fill="#1a2332">192.168.1.100</text>
  <text x="470" y="880" font-size="13" fill="#1a2332">山东·滨州</text>
  <text x="610" y="880" font-size="13" fill="#1a2332">Chrome/Windows</text>
  <rect x="790" y="868" width="60" height="16" rx="3" fill="url(#statusGreen)"/>
  <text x="820" y="880" text-anchor="middle" font-size="11" fill="#fff">正常</text>

  <!-- ============================================================ -->
  <!-- ===== 右侧:小滨AI助手提醒(替换原安全状态浮窗) ===== -->
  <!-- ============================================================ -->
  <rect x="720" y="120" width="240" height="195" rx="10" fill="#ffffff" stroke="#e0e4ec" filter="url(#shadowLight)"/>
  
  <!-- 头像 + 标题 -->
  <circle cx="745" cy="148" r="16" fill="url(#brandGrad)"/>
  <text x="745" y="153" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">小滨</text>
  <text x="770" y="153" font-size="14" font-weight="bold" fill="#1a2332">AI 助手</text>
  
  <!-- 对话气泡 -->
  <rect x="725" y="175" width="220" height="125" rx="8" fill="#f0f4ff" stroke="#d1e3ff"/>
  <path d="M740 175 L748 165 L756 175 Z" fill="#f0f4ff" stroke="#d1e3ff" stroke-width="1"/>
  
  <text x="740" y="198" font-size="12" fill="#1565c0" font-weight="bold">💡 安全状态</text>
  <text x="740" y="220" font-size="11" fill="#4a5568">• 密码强度:强</text>
  <text x="740" y="240" font-size="11" fill="#4a5568">• 手机/邮箱:已验证</text>
  <text x="740" y="260" font-size="11" fill="#4a5568">• 登录设备:2 台(含异常告警)</text>
  <text x="740" y="280" font-size="11" fill="#4a5568">• 近 90 天异常登录:1 次 ⚠️</text>
</svg>

(二)登录日志查看详细功能设计

1. 日志数据结构
字段 类型 说明
log_id VARCHAR(32) 日志ID
user_id VARCHAR(32) 供应商用户ID
login_time DATETIME 登录时间
logout_time DATETIME 登出/Token过期时间(可选)
ip_address VARCHAR(64) 登录IP
location VARCHAR(128) 地理位置(根据IP库解析)
user_agent VARCHAR(256) 设备/浏览器信息
device_fingerprint VARCHAR(128) 设备指纹(Canvas指纹,用于同一设备识别)
login_type VARCHAR(20) REGISTER(注册自动登录)/ PASSWORD(密码)/ SMS(短信验证码)/ TOKENRefreshToken
status VARCHAR(20) SUCCESS / FAIL / LOCKED
fail_reason VARCHAR(64) 登录失败原因(如“密码错误”)
is_current_session BOOLEAN 是否当前活跃会话
hash_chain VARCHAR(256) 审计哈希链指针

日志数据来源于 t_user_login_log 和 t_user_session 表,保留期限 ≥ 90 天(对外展示),后台归档 7 年。

2. 登录日志查看流程
flowchart TD
    A[供应商进入安全设置Tab] --> B[页面自动加载最近20条登录日志]
    B --> C[按登录时间倒序排列]
    C --> D[高亮显示异常登录记录]
    
    D --> E{用户操作}
    E -->|点击“刷新日志”| F[重新请求最新日志]
    E -->|点击“查看全部”| G[跳转至完整的日志列表页<br>(支持分页/筛选/导出)]
    E -->|点击“异常”标记| H[弹出详细告警提示]
    
    F --> B
    G --> I[完整日志页:支持按时间范围筛选]
    I --> J[支持按登录状态筛选<br>(全部/正常/异常/失败)]
    J --> K[支持按设备筛选]
    K --> L[支持导出为CSV(脱敏处理)]
    
    H --> M[展示:异常时间/IP/位置/设备]
    M --> N[提示建议:立即修改密码 / 检查账户绑定]
    N --> O[提供快捷跳转至“修改密码”]
    
    style D fill:#ffcdd2,stroke:#c62828
    style O fill:#e3f2fd,stroke:#1565c0
3. 异常登录判定规则(后台自动标注)
判定规则 说明 告警级别
地理位置突变 本次登录IP归属地与前5次登录归属地跨省/跨境,且时间间隔小于12小时 ⚠️ 高风险
设备指纹突变 本次登录设备指纹与历史记录完全不匹配 ⚠️ 中风险
非常规时间段 登录时间为北京时间 00:00-06:00 ⚠️ 中风险
登录失败连续 同一账户在5分钟内连续失败3次以上,随后成功登录 ⚠️ 中风险
IP黑名单 登录IP落入已知威胁IP库(由安全团队维护) 🔴 严重风险
暴力破解特征 同一IP尝试多个供应商账号登录 🔴 严重风险
4. 登录日志查询接口
项目 内容
路径 /api/v1/supplier/login-logs
方法 GET
认证 需要登录态

请求参数(分页 + 筛选)

参数名 类型 必填 说明
pageNo Integer 页码,默认1
pageSize Integer 每页条数,默认20,最大100
startDate String 开始时间(ISO 8601
endDate String 结束时间
status String 筛选:ALL / SUCCESS / FAIL / ABNORMAL
device String 设备筛选关键词
成功响应
{
  "code": "0",
  "data": {
    "total": 156,
    "pageNo": 1,
    "pageSize": 20,
    "abnormalCount": 2,
    "list": [
      {
        "logId": "LOG_20260701_001",
        "loginTime": "2026-07-01 09:30:25",
        "ipAddress": "192.168.1.100",
        "location": "山东·滨州",
        "device": "Chrome/Windows 10",
        "loginType": "PASSWORD",
        "status": "SUCCESS",
        "isCurrentSession": true,
        "isAbnormal": false
      },
      {
        "logId": "LOG_20260629_003",
        "loginTime": "2026-06-29 03:45:12",
        "ipAddress": "203.0.113.45",
        "location": "境外·未知",
        "device": "Firefox/Linux",
        "loginType": "PASSWORD",
        "status": "SUCCESS",
        "isCurrentSession": false,
        "isAbnormal": true,
        "abnormalReason": "地理位置突变"
      }
    ]
  }
}

(三)“安全设置”Tab 完整操作流程汇总

功能项 验证强度 是否同步SRM 频率限制 审计记录
修改密码 高(验证旧密码) 否(SRM侧密码独立) 每天≤5次失败锁定 完整哈希链
更换手机号 最高(旧+新双因子) 30天≤3次 完整哈希链
更换邮箱 最高(旧+新双因子) 30天≤3次 完整哈希链
踢出所有设备 中(登录态+二次确认) 无限制 完整哈希链
查看登录日志 低(仅登录态) 无限制 仅查询审计
异常告警反馈 无限制 生成反馈工单

(四)安全设置各子项处理流程

1. 修改密码流程(双因子验证旧密码)
flowchart TD
    A[点击“修改密码”] --> B[弹窗:输入旧密码 + 新密码 + 确认]
    B --> C[点击“确认修改”]
    C --> D[前端校验:非空/强度/新旧一致/新旧不同]
    D -->|校验失败| E[提示具体错误,表单高亮]
    D -->|校验通过| F[提交 PUT /api/v1/supplier/password]
    F --> G[后端:查询DB获取当前密码哈希]
    G --> H[Argon2id 比对旧密码]
    H -->|失败| I[失败计数 Redis + 1]
    I --> J{失败次数 ≥ 5?}
    J -->|是| K[锁定账号 30 分钟 + 告警]
    J -->|否| L[返回 401 剩余尝试次数]
    K --> M[提示“账户已锁定”]
    H -->|成功| N[重置失败计数]
    N --> O[新密码哈希后 UPDATE DB]
    O --> P[清除 Redis 中所有该用户的会话 Token]
    P --> Q[记录审计日志(哈希链)]
    Q --> R[发送通知至旧联系人(仅告知)]
    R --> S[返回成功,要求重新登录]
    S --> T[前端清除本地 Token,跳转登录页]
2. 更换手机号流程(双因子验证旧+新)
flowchart TD
    A[点击“更换手机号”] --> B[弹窗:Step1 验证旧手机]
    B --> C[发送验证码至旧手机]
    C --> D[用户输入验证码]
    D --> E[后端比对 Redis 验证码]
    E -->|失败| F[提示错误,可重试 5 次]
    E -->|成功| G[生成 oldOtpToken(有效期 5 分钟)]
    G --> H[Step2:输入新手机号]
    H --> I[点击“发送验证码”]
    I --> J[后端校验新手机号唯一性(未绑定其他账户)]
    J -->|已被占用| K[提示“该手机号已被其他供应商绑定”]
    J -->|可用| L[发送验证码至新手机]
    L --> M[用户输入新手机验证码]
    M --> N[后端比对 Redis 验证码]
    N -->|失败| O[提示错误]
    N -->|成功| P[生成 newOtpToken(有效期 5 分钟)]
    P --> Q[Step3:点击“确认变更”]
    Q --> R[提交 PUT /supplier/contact 带两个 Token]
    R --> S[后端校验两个 Token 有效性]
    S --> T[UPDATE DBcontact_phone 字段]
    T --> U[UPDATE DBlogin_name(若手机号为登录名)]
    U --> V[调用 SRM 更新接口]
    V --> W[记录审计日志(含新旧值)]
    W --> X[清除所有验证码缓存]
    X --> Y[发送通知至新旧手机号]
    Y --> Z[返回成功,保持登录态(旧 Token 仍有效)]
3. 更换邮箱流程(流程与手机号完全对称)

验证介质从“短信验证码”替换为“邮箱验证码”,调用 /api/v1/auth/email/send 接口,其余逻辑与更换手机号完全一致。

4. 踢出所有设备流程
flowchart TD
    A[点击“踢出所有设备”] --> B[二次确认弹窗]
    B -->|取消| C[关闭弹窗]
    B -->|确认| D[POST /api/v1/supplier/session/revoke-all]
    D --> E[后端:删除 Redis 中该用户所有 session_key]
    E --> F[记录审计日志]
    F --> G[返回成功]
    G --> H[前端清除本地 Token 跳转登录页]

2.1.3 Tab 3:资质文件

(一)页面 UI 设计

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 950" font-family="'Microsoft YaHei', sans-serif">
  <defs>
    <filter id="shadowLight" x="-2%" y="-2%" width="104%" height="104%">
      <feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.04"/>
    </filter>
    <linearGradient id="brandGrad" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#c2185b"/><stop offset="100%" stop-color="#e91e63"/>
    </linearGradient>
  </defs>

  <rect width="1100" height="950" fill="#f5f7fa"/>
  <rect x="60" y="30" width="980" height="890" rx="16" fill="#fff" filter="url(#shadowLight)"/>
  
  <!-- 页面标题 -->
  <text x="100" y="75" font-size="22" font-weight="bold" fill="#1a2332">企业信息管理</text>
  <text x="100" y="102" font-size="14" fill="#8895aa">管理您的企业资料、安全凭证和资质文件</text>
  <line x1="100" y1="125" x2="1000" y2="125" stroke="#f0f2f7" stroke-width="2"/>

  <!-- ===== Tab 切换栏(资质文件高亮) ===== -->
  <rect x="100" y="145" width="120" height="40" rx="8" fill="#f5f5f5"/>
  <text x="160" y="171" text-anchor="middle" font-size="14" fill="#666">基本资料</text>
  
  <rect x="230" y="145" width="120" height="40" rx="8" fill="#f5f5f5"/>
  <text x="290" y="171" text-anchor="middle" font-size="14" fill="#666">安全设置</text>
  
  <rect x="360" y="145" width="120" height="40" rx="8" fill="#c2185b"/>
  <text x="420" y="171" text-anchor="middle" font-size="14" font-weight="bold" fill="#fff">资质文件</text>

  <!-- ============================================================ -->
  <!-- ===== 1. 营业执照(固定,顶部展示) ===== -->
  <!-- ============================================================ -->
  <text x="140" y="215" font-size="15" font-weight="bold" fill="#4a5568">📄 营业执照</text>
  
  <rect x="140" y="235" width="820" height="95" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
  <text x="170" y="263" font-size="14" fill="#1a2332" font-weight="bold">营业执照_20260630.pdf</text>
  <text x="170" y="285" font-size="13" fill="#8895aa">版本:V3  上传时间:2026-06-30 14:30  AI识别:<tspan fill="#4caf50">✅ 已识别</tspan></text>
  <text x="170" y="308" font-size="13" fill="#8895aa">识别字段:企业全称、信用代码、法人、地址、经营范围</text>
  
  <!-- 营业执照操作按钮 -->
  <rect x="770" y="245" width="160" height="34" rx="6" fill="url(#brandGrad)"/>
  <text x="850" y="267" text-anchor="middle" font-size="13" font-weight="bold" fill="#fff">📤 更新营业执照</text>
  <rect x="770" y="285" width="160" height="34" rx="6" fill="#e0e4ec"/>
  <text x="850" y="307" text-anchor="middle" font-size="13" fill="#666">📋 查看历史版本</text>

  <!-- ============================================================ -->
  <!-- ===== 2. 其他资质文件 ===== -->
  <!-- ============================================================ -->
  <line x1="140" y1="355" x2="1000" y2="355" stroke="#f0f2f7" stroke-width="1"/>
  <text x="140" y="390" font-size="15" font-weight="bold" fill="#4a5568">📎 其他资质文件</text>
  <text x="140" y="415" font-size="13" fill="#8895aa">ISO认证、安全生产许可证、产品检测报告、行业资质证书等</text>
  
  <!-- 上传按钮 -->
  <rect x="770" y="375" width="160" height="34" rx="6" fill="url(#brandGrad)"/>
  <text x="850" y="397" text-anchor="middle" font-size="13" font-weight="bold" fill="#fff">+ 上传资质文件</text>

  <!-- 文件列表 -->
  <rect x="140" y="435" width="820" height="110" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
  
  <!-- 表头 -->
  <rect x="140" y="435" width="820" height="34" rx="8" fill="#f0f2f7"/>
  <text x="170" y="457" font-size="12" font-weight="bold" fill="#4a5568">文件名称</text>
  <text x="400" y="457" font-size="12" font-weight="bold" fill="#4a5568">上传时间</text>
  <text x="550" y="457" font-size="12" font-weight="bold" fill="#4a5568">大小</text>
  <text x="650" y="457" font-size="12" font-weight="bold" fill="#4a5568">操作</text>

  <!-- 行1 -->
  <rect x="150" y="475" width="800" height="32" rx="4" fill="#fff"/>
  <text x="170" y="497" font-size="13" fill="#1a2332">ISO9001_质量管理体系认证.pdf</text>
  <text x="400" y="497" font-size="13" fill="#666">2026-05-20</text>
  <text x="550" y="497" font-size="13" fill="#666">2.3MB</text>
  <text x="660" y="497" font-size="13" fill="#1565c0" text-decoration="underline">重命名</text>
  <text x="740" y="497" font-size="13" fill="#f44336" text-decoration="underline">删除</text>

  <!-- 行2 -->
  <rect x="150" y="510" width="800" height="32" rx="4" fill="#fff"/>
  <text x="170" y="532" font-size="13" fill="#1a2332">安全生产许可证_2024.pdf</text>
  <text x="400" y="532" font-size="13" fill="#666">2025-12-10</text>
  <text x="550" y="532" font-size="13" fill="#666">1.8MB</text>
  <text x="660" y="532" font-size="13" fill="#1565c0" text-decoration="underline">重命名</text>
  <text x="740" y="532" font-size="13" fill="#f44336" text-decoration="underline">删除</text>

  <!-- ============================================================ -->
  <!-- ===== 3. 业绩证明文件 ===== -->
  <!-- ============================================================ -->
  <line x1="140" y1="570" x2="1000" y2="570" stroke="#f0f2f7" stroke-width="1"/>
  <text x="140" y="605" font-size="15" font-weight="bold" fill="#4a5568">🏆 业绩证明文件</text>
  <text x="140" y="630" font-size="13" fill="#8895aa">近三年同类项目合同、中标通知书、验收报告、用户评价等</text>
  
  <!-- 上传按钮 -->
  <rect x="770" y="590" width="160" height="34" rx="6" fill="url(#brandGrad)"/>
  <text x="850" y="612" text-anchor="middle" font-size="13" font-weight="bold" fill="#fff">+ 上传业绩文件</text>

  <!-- 文件列表 -->
  <rect x="140" y="650" width="820" height="145" rx="8" fill="#fafbfc" stroke="#e0e4ec"/>
  
  <!-- 表头 -->
  <rect x="140" y="650" width="820" height="34" rx="8" fill="#f0f2f7"/>
  <text x="170" y="672" font-size="12" font-weight="bold" fill="#4a5568">文件名称</text>
  <text x="400" y="672" font-size="12" font-weight="bold" fill="#4a5568">上传时间</text>
  <text x="550" y="672" font-size="12" font-weight="bold" fill="#4a5568">大小</text>
  <text x="650" y="672" font-size="12" font-weight="bold" fill="#4a5568">操作</text>

  <!-- 行1 -->
  <rect x="150" y="690" width="800" height="32" rx="4" fill="#fff"/>
  <text x="170" y="712" font-size="13" fill="#1a2332">滨化集团_催化剂供货合同_2025.pdf</text>
  <text x="400" y="712" font-size="13" fill="#666">2025-08-15</text>
  <text x="550" y="712" font-size="13" fill="#666">5.6MB</text>
  <text x="660" y="712" font-size="13" fill="#1565c0" text-decoration="underline">重命名</text>
  <text x="740" y="712" font-size="13" fill="#f44336" text-decoration="underline">删除</text>

  <!-- 行2 -->
  <rect x="150" y="725" width="800" height="32" rx="4" fill="#fff"/>
  <text x="170" y="747" font-size="13" fill="#1a2332">万华化学_项目验收报告_2024.pdf</text>
  <text x="400" y="747" font-size="13" fill="#666">2024-12-20</text>
  <text x="550" y="747" font-size="13" fill="#666">3.2MB</text>
  <text x="660" y="747" font-size="13" fill="#1565c0" text-decoration="underline">重命名</text>
  <text x="740" y="747" font-size="13" fill="#f44336" text-decoration="underline">删除</text>

  <!-- 行3 空状态提示 -->
  <rect x="150" y="760" width="800" height="32" rx="4" fill="#fafafa"/>
  <text x="170" y="782" font-size="13" fill="#b0bbcc">该分类下暂无其他文件</text>

  <!-- ============================================================ -->
  <!-- ===== 右侧:小滨AI助手提醒(整体上移50 ===== -->
  <!-- ============================================================ -->
  <rect x="720" y="150" width="240" height="195" rx="10" fill="#ffffff" stroke="#e0e4ec" filter="url(#shadowLight)"/>
  
  <!-- 头像 + 标题 -->
  <circle cx="745" cy="178" r="16" fill="url(#brandGrad)"/>
  <text x="745" y="183" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">小滨</text>
  <text x="770" y="183" font-size="14" font-weight="bold" fill="#1a2332">AI 助手</text>
  
  <!-- 对话气泡 -->
  <rect x="725" y="205" width="220" height="125" rx="8" fill="#f0f4ff" stroke="#d1e3ff"/>
  <path d="M740 205 L748 195 L756 205 Z" fill="#f0f4ff" stroke="#d1e3ff" stroke-width="1"/>
  
  <text x="740" y="228" font-size="12" fill="#1565c0" font-weight="bold">📌 文件管理说明</text>
  <text x="740" y="250" font-size="11" fill="#4a5568">• 支持格式:PDF/JPG/PNG</text>
  <text x="740" y="270" font-size="11" fill="#4a5568">• 单个文件 ≤ 20MB</text>
  <text x="740" y="290" font-size="11" fill="#4a5568">• 文件总数量 ≤ 50 个</text>
  <text x="740" y="310" font-size="11" fill="#4a5568">• 删除后不可恢复,请谨慎操作</text>
  <text x="740" y="330" font-size="11" fill="#4a5568">• 投标报名时可一键引用已上传文件</text>
</svg>

(二)完整数据模型

资质文件表 t_supplier_qualification
字段名 类型 约束 说明
id BIGINT(20) PK, AUTO_INCREMENT 自增主键
file_id VARCHAR(32) UNIQUE, NOT NULL 文件业务IDQ_YYYYMMDD_XXXX
supplier_id VARCHAR(32) FK, NOT NULL 供应商ID
category VARCHAR(32) NOT NULL 分类LICENSE / OTHER_QUALIFICATION / PERFORMANCE
file_name VARCHAR(256) NOT NULL 文件显示名称(可修改)
file_original_name VARCHAR(256) NOT NULL 原始文件名(上传时的名称,不可修改)
file_url VARCHAR(512) NOT NULL OSS存储URL
file_size BIGINT 文件大小(字节)
file_type VARCHAR(16) MIME类型(application/pdf, image/jpeg等)
upload_time DATETIME NOT NULL 上传时间
last_modified_time DATETIME 最后修改时间(重命名时更新)
version INT DEFAULT 1 版本号(仅营业执照使用,其他文件固定为1)
status VARCHAR(20) NOT NULL ACTIVE / DELETED(软删除)
deleted_time DATETIME 删除时间
created_at DATETIME 创建时间
updated_at DATETIME 更新时间
营业执照单独管理(专用字段,与上表冗余关联)

营业执照固定存在且唯一,使用t_supplier_profile中的字段存储主版本,历史版本通过license_history JSON字段保存,方便追溯。

供应商主表字段 说明
license_file_url 当前版本营业执照文件URL
license_version 当前版本号
license_ocr_result OCR识别结果JSON
license_history 历史版本数组:[{version, url, ocr, upload_time}]

(三)详细处理流程

1. 上传资质/业绩文件
flowchart TD
    subgraph 供应商端
        A[点击“上传资质文件”或“上传业绩文件”] --> B[弹出文件选择窗口]
        B --> C[选择本地文件]
        C --> D[前端校验:格式/大小]
        D -->|不合格| E[提示错误:格式不支持或文件过大]
        D -->|合格| F[点击“确认上传”]
        F --> G[显示上传进度条]
        G --> H[调用 POST /api/v1/supplier/qualification/upload]
        H --> I[等待后端响应]
        I --> J{响应结果}
        J -->|成功| K[列表新增一行,显示新文件]
        J -->|失败| L[提示具体错误原因]
        K --> M[完成]
    end
    subgraph STP后端
        STP1[接收上传请求] --> STP2[校验供应商权限]
        STP2 --> STP3[校验文件数量是否超限]
        STP3 -->|超限| STP4[返回错误:文件数超50]
        STP3 -->|未超限| STP5[流式上传至OSS]
        STP5 --> STP6[获取文件URL]
        STP6 --> STP7[INSERT t_supplier_qualification]
        STP7 --> STP8[记录审计日志]
        STP8 --> STP9[返回成功 + 文件元数据]
    end
    subgraph 外部系统
        OSS[OSS对象存储]
    end
    STP5 --> OSS

关键校验规则(上传)

校验项 规则
文件格式 JPG、PNG、PDFMIME类型校验)
文件大小 ≤ 20MB
总数量限制 每个分类(资质/业绩)各 ≤ 30 个,总计 ≤ 50 个
文件名称 自动去除特殊字符,防止路径遍历攻击
文件去重 校验MD5,避免同一文件重复上传(提示“该文件已存在”)

接口定义

项目 内容
路径 /api/v1/supplier/qualification/upload
方法 POST
认证 需要登录态
请求 multipart/form-data
表单参数 类型 必填 说明
file File 文件内容
category String OTHER_QUALIFICATION / PERFORMANCE
fileName String 自定义显示名称(不传则使用原始文件名)

成功响应

{
  "code": "0",
  "data": {
    "fileId": "Q_20260701_001",
    "fileName": "ISO9001_质量管理体系认证.pdf",
    "fileUrl": "https://oss.stp.com/...",
    "fileSize": 2411724,
    "uploadTime": "2026-07-01 10:30:00"
  }
}
2. 重命名文件
flowchart TD
    A[点击文件行的“重命名”] --> B[文件名变为可编辑输入框]
    B --> C[供应商输入新名称]
    C --> D[点击“保存”或按回车]
    D --> E[前端校验:非空、长度≤100、不含特殊字符]
    E -->|校验失败| F[提示具体错误]
    E -->|校验通过| G[调用 PUT /api/v1/supplier/qualification/rename]
    G --> H[后端校验权限:文件归属当前供应商]
    H --> I[更新 file_name 字段]
    I --> J[更新 last_modified_time]
    J --> K[记录审计日志]
    K --> L[返回成功]
    L --> M[页面刷新显示新名称]

接口定义

项目 内容
路径 /api/v1/supplier/qualification/rename
方法 PUT
认证 需要登录态

请求体


{
  "fileId": "Q_20260701_001",
  "newName": "ISO9001_质量管理体系认证证书_2026版.pdf"
}
3. 删除文件
flowchart TD
    A[点击文件行的“删除”] --> B[弹出二次确认弹窗]
    B -->|取消| C[关闭弹窗]
    B -->|确认| D[调用 DELETE /api/v1/supplier/qualification/delete]
    D --> E[后端校验权限]
    E --> F[检查文件是否被正在进行的投标报名引用]
    F -->|被引用| G[返回错误:文件正在使用中,不可删除]
    F -->|未被引用| H[软删除:status='DELETED' + deleted_time]
    H --> I[记录审计日志(含文件名和删除人)]
    I --> J[返回成功]
    J --> K[前端移除该行]

特别规则

  • 软删除仅标记状态,OSS文件暂不物理删除(保留期≥90天,便于审计追溯)。
  • 若文件已被进行中的投标报名引用,则不允许删除,提示“该文件已被报名项目引用,无法删除”。

接口定义

项目 内容
路径 /api/v1/supplier/qualification/delete
方法 DELETE
认证 需要登录态

请求体

{
  "fileId": "Q_20260701_001"
}
4. 营业执照更新

营业执照更新逻辑与资质文件独立,使用专门接口。

flowchart TD
    subgraph 供应商端
        A[点击“更新营业执照”] --> B[上传新营业执照文件]
        B --> C[显示“上传中...AI审查中...”]
        C --> D{等待后端最终结果}
        D -->|审查通过| E[显示成功:营业执照已更新]
        D -->|审查不通过| F[显示具体驳回原因<br>+ 重新上传按钮]
        E --> G[页面自动刷新,显示新信息]
        F --> H[供应商修改后重新上传]
    end

    subgraph STP后端处理链路
        STP1[接收上传请求] --> STP2[文件病毒扫描 + OSS上传]
        STP2 --> STP3[调用 OCR 服务提取文字字段]
        STP3 --> STP4[AI 真实性审查(见下方子流程)]
        STP4 --> STP5{真实性审查通过?}
        STP5 -->|否| STP6[驳回:营业执照疑似伪造或篡改]
        STP5 -->|是| STP7[AI 一致性审查(与数据库/工商库比对)]
        STP7 --> STP8{一致性审查通过?}
        STP8 -->|否| STP9[驳回:信息不一致,需人工复核]
        STP8 -->|是| STP10[生成审查报告 + 数字指纹]
        STP10 --> STP11[更新供应商主表企业字段]
        STP11 --> STP12[保存历史版本至 license_history]
        STP12 --> STP13[调用 SRM 批量更新接口]
        STP13 --> STP14[记录审计日志(含审查报告哈希)]
        STP14 --> STP15[返回成功]
        
        STP6 --> STP16[记录审计日志(含失败原因)]
        STP9 --> STP16
        STP16 --> STP17[返回驳回 + 原因]
    end

    subgraph AI审查引擎
        AI1[真实性审查] --> AI1a[图像真伪检测<br>PS/拼接/翻拍识别)]
        AI1 --> AI1b[印章/水印检测]
        AI1 --> AI1c[版面一致性检测]
        AI1a --> AI2[输出:真实/可疑/伪造]
        AI1b --> AI2
        AI1c --> AI2
        
        AI3[一致性审查] --> AI3a[OCR字段 vs 数据库现有字段]
        AI3 --> AI3b[OCR字段 vs 工商API返回数据]
        AI3a --> AI4[输出:一致/部分不一致/严重不一致]
        AI3b --> AI4
    end

    subgraph 外部系统
        EXT1[OSS对象存储]
        EXT2[工商信息API]
        EXT3[SRM系统]
    end
    
    STP2 --> EXT1
    STP3 --> STP4
    AI3b --> EXT2
    STP13 --> EXT3
5. 营业执照历史版本查看

点击“查看历史版本”弹出时间线,展示所有历史版本:

V3 (当前) - 2026-06-30 14:30  滨化新材料有限公司_2026.pdf  ✅ 当前使用
V2         - 2025-12-01 09:20  滨化新材料有限公司_2025.pdf
V1         - 2024-06-15 11:00  滨化新材料有限公司_2024.pdf

每个版本可预览/下载。

(四)业务规则汇总

规则ID 规则 适用范围
R01 营业执照全局唯一,供应商仅可维护一份,版本递增。 营业执照
R02 营业执照信用代码识别结果与系统不一致时,系统不自动更新,提示人工复核。 营业执照
R03 其他资质文件分类:自动归类为OTHER_QUALIFICATIONPERFORMANCE 资质/业绩
R04 同一分类文件数量上限为30个,总上限50个。 资质/业绩
R05 文件格式仅支持PDF/JPG/PNG,大小≤20MB。 资质/业绩
R06 文件重命名后需同步更新last_modified_time 资质/业绩
R07 删除为软删除,数据永久保留(不物理删除OSS文件,保留≥90天)。 资质/业绩
R08 被进行中的投标报名引用的文件不可删除。 资质/业绩
R09 所有上传/删除/重命名操作记录审计日志(哈希链)。 全场景
R10 资质/业绩文件不自动同步至SRM(由供应商自主管理,报名时按需引用)。 资质/业绩
R11 营业执照更新后自动同步至SRM,保持企业信息一致。 营业执照

(五)异常场景处理矩阵

异常场景 用户侧表现 后端处理
上传文件格式不支持 前端提示“仅支持PDF/JPG/PNG格式” 返回400
上传文件超过20MB 前端提示“文件不能超过20MB” 返回400
文件数量达到上限(50个) 提示“文件数量已满(50个),请删除旧文件后再上传” 返回429
重命名时名称重复 提示“该名称已存在,请修改” 返回409
重命名时含非法字符 提示“文件名不能包含特殊字符” 返回400
删除时文件被报名引用 提示“该文件已被项目XXX引用,无法删除” 返回403
营业执照OCR识别失败 提示“识别失败,请确保图片清晰” 返回错误,允许用户重试或手动修改
营业执照信用代码不一致 页面高亮告警“识别结果与系统记录不一致,请人工确认” 记录告警日志
OSS上传超时(>10s 提示“上传超时,请重试” 记录WARN日志
翻拍检测报警 “请上传营业执照原件扫描件,而非照片/复印件” 驳回
信用代码与工商API不匹配 “统一社会信用代码与工商登记信息不一致,请核实后重新上传” 驳回,记录差异
OCR识别结果与数据库冲突 “识别结果与系统记录不一致,进入人工复核流程” 冻结变更,生成待办
AI审查服务超时(>20s “审查服务繁忙,请稍后重试” 返回503,不更新任何数据
工商API不可用 “企业信息核验服务暂不可用,请稍后重试” 阻断更新(不降级),记录告警

3. SRM同步与异常重试

8.1 同步触发条件

变更类型 调用SRM接口 同步内容
修改密码 不调用SRM 无(SRM侧密码独立管理)
修改联系人姓名 POST /contact/update contactName
修改手机号 POST /contact/update contactPhone
修改邮箱 POST /contact/update contactEmail
更新营业执照 POST /supplier/update companyName, address, legalPerson等(若变更)

8.2 重试机制

stateDiagram-v2
    [*] --> SYNC_PENDING: 变更提交
    SYNC_PENDING --> SYNCING: 立即调用SRM
    SYNCING --> SYNC_SUCCESS: 成功
    SYNCING --> SYNC_FAILED: 失败
    SYNC_FAILED --> SYNCING: 异步重试(间隔5/15/30/60/120分钟)
    SYNC_FAILED --> ALERT: 5次均失败
    ALERT --> [*]: 管理员人工介入

9. 异常场景处理矩阵

异常场景 用户侧表现 后端处理
修改密码时旧密码错误 提示“当前密码错误,剩余X次机会” 失败计数+1,写入审计日志
密码错误达5次 提示“账户已锁定30分钟” 锁定并告警
修改手机号时新号已被占用 提示“该手机号已被其他供应商绑定” 返回409冲突
验证码发送频率过高 提示“发送过于频繁,请60秒后重试” 返回429限流
双因子验证Token超时(5分钟) 提示“验证已过期,请重新获取” 清除旧Token,要求重发
营业执照OCR识别失败 提示“识别失败,请检查图片清晰度” 允许手动填写关键字段并标记
SRM同步超时(>5s) 用户无感知(显示成功) 异步重试,后台告警
上传文件格式/大小不符 前端拦截,提示具体原因 后端二次校验

10. 通知策略

触发事件 通知方式 接收方 文案
密码修改成功 短信+邮件 当前联系人与旧联系人(若变更) “您的STP账户密码已于XX时间修改,若非本人操作请立即联系管理员。”
手机号修改成功 短信+邮件 新旧手机号 “您的STP账户绑定手机号已变更为XXX,请确认。”
邮箱修改成功 邮件 新旧邮箱 “您的STP账户绑定邮箱已变更为XXX。”
营业执照更新成功 站内信 供应商联系人 “企业营业执照已更新,请留意后续通知。”
SRM同步失败告警 邮件+飞书 系统管理员 “供应商ID {id} 信息变更同步SRM失败,请人工介入。”