配套面试准备:完成本篇后,可以继续阅读:Day02_SRM核心业务面试准备 Day02_SRM供应商管理系统完整面试指南
课程目标:深度理解供应链最上游的业务逻辑,完整实现供应商全生命周期管理系统,包括信息管理、审核流程、文件上传、绩效评分和供应商协同 Portal。
今日交付物:
- 供应商数据库表设计(5 张表)完整 SQL
- 供应商 CRUD 接口设计(RESTful 风格)ø
- 供应商审核工作流(状态机)
- 文件上传模块(本地 + OSS 双模式)
- 供应商绩效评分定时计算逻辑
- 供应商 Portal 账号体系
- 前端页面结构与交互设计
第一节 供应商管理业务全解析
1.1 为什么供应商管理是供应链的起点
跨境出海的整个供应链,货物的源头来自供应商。没有稳定、可靠的供应商体系,后续所有的采购、仓储、发货都是无源之水。
一个真实的业务场景:
某卖家在亚马逊卖耳机,某天一款产品突然爆单,从日均50单飙升到日均500单。
- 如果只有1家供应商且供应商产能有限 → 缺货断销,爆款白白浪费
- 如果有3家备选供应商且评级都在A级以上 → 立即切换到产能更大的供应商,完美承接爆款
这就是供应商管理系统(SRM)的核心价值:
graph TD
A["没有SRM<br>供应商管理混乱"] --> A1["供应商信息散落在Excel/微信"]
A --> A2["不知道哪家供应商质量好"]
A --> A3["出了问题找不到责任方"]
A --> A4["突发需求无法快速切换供应商"]
B["有SRM<br>供应商管理规范"] --> B1["所有供应商信息集中管理<br>随时查阅"]
B --> B2["绩效评分体系<br>数据说话选最优"]
B --> B3["操作全留记录<br>问题可追溯"]
B --> B4["多家备选供应商<br>快速响应需求变化"]
1.2 供应商的业务分类
在跨境电商场景中,供应商并非只有”卖货给我们的工厂”这一种,完整的供应商类型包括:
| 供应商类型 | 代码值 | 业务角色 | 举例 |
|---|---|---|---|
| 工厂供应商 | 1 | 直接生产商品,价格最低,MOQ较高,交期较长 | 广州电子厂、义乌小商品厂 |
| 贸易商 | 2 | 中间商,聚合多家工厂货源,MOQ灵活,价格略高 | 广州综合贸易公司 |
| 物流服务商 | 3 | 提供国际物流服务,不提供商品 | 顺丰国际、递四方、云途物流 |
你可以把工厂供应商理解为”批发市场的厂家直销摊位”,贸易商理解为”中间商贩”,物流服务商理解为”快递公司”。
MOQ = Minimum Order Quantity,最小起订量。 比如某工厂说 MOQ = 1000 件,意思是你每次采购至少要下 1000 件,低于这个数量工厂不接单,或者单价会明显变高。 工厂供应商通常价格低,但 MOQ 高;贸易商 MOQ 灵活,但价格略高。
1.3 供应商全生命周期
供应商在系统中经历的完整生命周期:
flowchart LR
A(["开始"]) --> B["📝 注册引入<br>采购专员录入供应商信息"]
B --> C["🔍 资质审核<br>核实营业执照、质检报告"]
C --> D["✅ 合作上线<br>签订合同、开通Portal账号"]
D --> E["🛒 日常合作<br>接收采购单、发货、对账"]
E --> F["📊 绩效评估<br>每月自动计算综合评分"]
F --> G{"评分结果"}
G -->|"持续优秀"| H["🏆 晋级优质<br>增加合作份额"]
G -->|"持续下滑"| I["⚠️ 预警改进<br>限制新采购单"]
G -->|"严重不达标"| J["🚫 淘汰处理<br>停用账号、转移订单"]
H --> E
I --> E
J --> K(["结束"])
SaaS是服务于卖家的 不直接跟供应商联系 卖家在我们的系统入职之后 他们自己可以去联系供应商 (微信/打电话/线下) 当卖家联系好了供应商 就可以在我们的系统中 录入这个供应商的信息
采购专员 卖家中的一个角色
资质审核 —> 卖家审核
Portal账号 —> 专门给供应商开的一个管理平台
预警改进 / 淘汰处理 都仅仅只是SaaS平台给卖家的一个提醒/通知 具体是否要淘汰 还是取决于卖家自己
1.4 供应商管理涉及的角色
| 角色 | 在SRM中的操作权限 |
|---|---|
| 采购专员 | 新增/编辑供应商信息,查看供应商列表,发起审核申请 |
| 采购负责人 | 审核供应商(通过/拒绝),查看绩效报告,停用/启用供应商 |
| 财务专员 | 查看供应商结算信息,维护银行账户信息 |
| 供应商(外部) | 登录Portal查看自己的采购单、交货记录、账款信息,上传发货单据 |
第二节 供应商数据库表设计
2.1 供应商模块表结构总览
SRM 模块共涉及 5 张核心数据表,各表之间的关系如下:
erDiagram
supplier {
bigint id PK "供应商主键"
bigint tenant_id "租户ID"
varchar supplier_code "供应商编码"
varchar supplier_name "供应商名称"
tinyint supplier_type "类型1工厂2贸易3物流"
tinyint status "状态0草稿1待审2通过3拒绝4停用"
char grade "评级SABC"
decimal score "综合评分0-100"
}
supplier_cert {
bigint id PK "资质文件主键"
bigint supplier_id FK "关联供应商"
tinyint cert_type "资质类型"
varchar file_name "文件名称"
varchar file_url "文件存储URL"
date expire_date "资质有效期"
}
supplier_contact {
bigint id PK "联系人主键"
bigint supplier_id FK "关联供应商"
varchar contact_name "联系人姓名"
varchar position "职位"
tinyint is_primary "是否主联系人"
}
supplier_score_log {
bigint id PK "评分记录主键"
bigint supplier_id FK "关联供应商"
varchar score_month "评分月份YYYYMM"
decimal delivery_rate "准时到货率"
decimal quality_rate "质量合格率"
decimal total_score "综合得分"
}
supplier_audit_log {
bigint id PK "审核记录主键"
bigint supplier_id FK "关联供应商"
tinyint from_status "变更前状态"
tinyint to_status "变更后状态"
varchar audit_remark "审核意见"
bigint operator_id "操作人"
}
supplier ||--o{ supplier_cert : "拥有多个资质文件"
supplier ||--o{ supplier_contact : "拥有多个联系人"
supplier ||--o{ supplier_score_log : "拥有多条评分记录"
supplier ||--o{ supplier_audit_log : "拥有多条审核记录"
2.2 供应商主表 SQL
-- ============================================================
-- 供应商主表
-- 存储供应商的全部基础信息和当前状态
-- ============================================================
CREATE TABLE `supplier`
(
-- ==================== 主键与租户 ====================
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID,雪花算法生成',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID,多租户隔离核心字段',
-- ==================== 供应商编号 ====================
`supplier_code` VARCHAR(32) NOT NULL COMMENT '供应商编码,系统自动生成,格式:SUP-YYYYMMDD-XXXX,全局唯一',
-- ==================== 基础信息 ====================
`supplier_name` VARCHAR(128) NOT NULL COMMENT '供应商公司名称(全称)',
`supplier_type` TINYINT NOT NULL COMMENT '供应商类型:1=工厂供应商 2=贸易商 3=物流服务商',
`category_ids` JSON NULL COMMENT '供货品类ID列表,如[1,5,12],对应商品品类表',
`province` VARCHAR(32) NULL COMMENT '所在省份,如:广东省',
`city` VARCHAR(32) NULL COMMENT '所在城市,如:广州市',
`address` VARCHAR(256) NULL COMMENT '详细地址,含街道门牌号',
`website` VARCHAR(256) NULL COMMENT '公司官方网站URL',
`company_size` TINYINT NULL COMMENT '公司规模:1=50人以下 2=50-200人 3=200-500人 4=500人以上',
`founded_year` SMALLINT NULL COMMENT '成立年份,如:2015',
-- ==================== 主联系人 ====================
`contact_name` VARCHAR(64) NOT NULL COMMENT '主联系人姓名',
`contact_phone` VARCHAR(20) NOT NULL COMMENT '主联系人手机号',
`contact_email` VARCHAR(128) NOT NULL COMMENT '主联系人邮箱,供应商Portal登录账号',
`contact_wechat` VARCHAR(64) NULL COMMENT '联系人微信号',
`contact_whatsapp` VARCHAR(64) NULL COMMENT '联系人WhatsApp账号(海外供应商)',
-- ==================== 财务信息 ====================
`bank_name` VARCHAR(64) NULL COMMENT '开户银行名称,如:招商银行广州分行',
`bank_account` VARCHAR(64) NULL COMMENT '银行账号,AES-256加密存储',
`bank_account_name` VARCHAR(64) NULL COMMENT '银行开户名(与营业执照一致)',
`tax_no` VARCHAR(32) NULL COMMENT '纳税人识别号(统一社会信用代码)',
`invoice_type` TINYINT NULL COMMENT '开票类型:1=增值税专用发票 2=增值税普通发票 3=收据',
-- ==================== 供货能力 ====================
`moq` INT NULL COMMENT 'Minimum Order Quantity,最小起订量,单位:件',
`lead_time_days` INT NULL COMMENT '交货周期(天),从下单到发货的天数',
`monthly_capacity` INT NULL COMMENT '月最大供货量(件),用于评估是否能承接大单',
`currency` CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '结算货币:CNY=人民币 USD=美元 EUR=欧元',
`payment_days` INT NOT NULL DEFAULT 0 COMMENT '账期天数:0=现款现货 30=月结30天 60=月结60天',
-- ==================== 评级与评分 ====================
`grade` CHAR(1) NOT NULL DEFAULT 'C' COMMENT '综合评级:S=优质(90+) A=良好(75-89) B=一般(60-74) C=预警(60以下)',
`score` DECIMAL(5, 2) NOT NULL DEFAULT 0.00 COMMENT '最新综合评分,满分100分,由定时任务每月自动更新',
`last_score_month` VARCHAR(6) NULL COMMENT '最后一次评分的月份,格式YYYYMM,如202501',
-- ==================== 审核信息 ====================
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=草稿 1=待审核 2=已通过 3=已拒绝 4=已停用',
`audit_user_id` BIGINT NULL COMMENT '最后一次审核操作人的用户ID',
`audit_time` DATETIME NULL COMMENT '最后一次审核时间',
`audit_remark` VARCHAR(512) NULL COMMENT '最后一次审核的意见(拒绝时必填)',
-- ==================== Portal账号 ====================
`portal_user_id` BIGINT NULL COMMENT '供应商Portal登录账号的用户ID,关联sys_user表',
`portal_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Portal是否已开通:0=未开通 1=已开通',
-- ==================== 其他 ====================
`remark` VARCHAR(512) NULL COMMENT '内部备注(供应商看不到)',
`tags` JSON NULL COMMENT '供应商标签列表,如["重点合作","质量稳定","响应快"]',
-- ==================== 公共字段 ====================
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,自动填充',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间,自动填充',
`create_by` BIGINT NULL COMMENT '创建人的用户ID',
`update_by` BIGINT NULL COMMENT '最后修改人的用户ID',
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常 1=已删除',
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新自动+1',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_supplier_code` (`tenant_id`, `supplier_code`) COMMENT '同一租户内供应商编码唯一',
KEY `idx_tenant_status` (`tenant_id`, `status`) COMMENT '按租户+状态筛选',
KEY `idx_tenant_grade` (`tenant_id`, `grade`) COMMENT '按租户+评级筛选',
KEY `idx_tenant_type` (`tenant_id`, `supplier_type`) COMMENT '按租户+类型筛选',
KEY `idx_create_time` (`create_time`) COMMENT '按创建时间排序'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci
COMMENT = '供应商主表';
2.3 供应商资质文件表 SQL
-- ============================================================
-- 供应商资质文件表
-- 每个供应商可上传多种、多份资质文件
-- ============================================================
CREATE TABLE `supplier_cert`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`supplier_id` BIGINT NOT NULL COMMENT '关联供应商ID,外键关联 supplier.id',
`cert_type` TINYINT NOT NULL COMMENT '资质类型:1=营业执照 2=质检报告 3=产品认证(CE/FCC/UL) 4=银行账户证明 5=其他',
`cert_name` VARCHAR(128) NOT NULL COMMENT '资质名称,如:营业执照、CE认证报告',
`file_name` VARCHAR(256) NOT NULL COMMENT '原始文件名称(上传时的文件名)',
`file_url` VARCHAR(512) NOT NULL COMMENT '文件存储URL(OSS路径或本地路径)',
`file_size` BIGINT NOT NULL DEFAULT 0 COMMENT '文件大小,单位:字节(Byte)',
`file_type` VARCHAR(32) NOT NULL COMMENT '文件类型(MIME类型):application/pdf / image/jpeg / image/png',
`issue_date` DATE NULL COMMENT '资质颁发日期',
`expire_date` DATE NULL COMMENT '资质有效期截止日期,NULL表示永久有效',
`is_expired` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已过期:0=有效 1=已过期,由定时任务每日更新',
`cert_no` VARCHAR(64) NULL COMMENT '证书编号(如营业执照注册号)',
`remark` VARCHAR(256) NULL COMMENT '备注说明',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
`create_by` BIGINT NULL COMMENT '上传人的用户ID',
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
PRIMARY KEY (`id`),
KEY `idx_supplier_id` (`supplier_id`) COMMENT '按供应商查询其所有资质',
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_expire_date` (`expire_date`) COMMENT '按到期日期查询,用于资质到期预警'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci
COMMENT = '供应商资质文件表';
2.4 供应商联系人表 SQL
-- ============================================================
-- 供应商联系人表
-- 一个供应商可有多个联系人(主联系人 + 业务联系人 + 财务联系人等)
-- 主联系人冗余存储在 supplier 主表中方便快速查询
-- ============================================================
CREATE TABLE `supplier_contact`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`supplier_id` BIGINT NOT NULL COMMENT '关联供应商ID',
`contact_name` VARCHAR(64) NOT NULL COMMENT '联系人姓名',
`position` VARCHAR(64) NULL COMMENT '职位/职称,如:销售经理、财务总监',
`phone` VARCHAR(20) NULL COMMENT '手机号码',
`email` VARCHAR(128) NULL COMMENT '邮箱地址',
`wechat` VARCHAR(64) NULL COMMENT '微信号',
`whatsapp` VARCHAR(64) NULL COMMENT 'WhatsApp账号',
`department` VARCHAR(64) NULL COMMENT '所在部门,如:销售部、财务部',
`is_primary` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否主联系人:1=是 0=否,每个供应商只能有1个主联系人',
`contact_type` TINYINT NOT NULL DEFAULT 1 COMMENT '联系人类型:1=业务对接 2=财务对账 3=技术支持 4=紧急联系',
`remark` VARCHAR(256) NULL COMMENT '备注',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_by` BIGINT NULL,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_supplier_id` (`supplier_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci
COMMENT = '供应商联系人表';
2.5 供应商评分记录表 SQL
-- ============================================================
-- 供应商月度评分记录表
-- 每月由定时任务自动计算并写入一条记录,只增不改
-- ============================================================
CREATE TABLE `supplier_score_log`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`supplier_id` BIGINT NOT NULL COMMENT '关联供应商ID',
`score_month` VARCHAR(6) NOT NULL COMMENT '评分月份,格式YYYYMM,如202501',
-- ==================== 各维度原始数据 ====================
`total_orders` INT NOT NULL DEFAULT 0 COMMENT '本月采购订单总数(用于计算各项比率)',
`delivered_on_time` INT NOT NULL DEFAULT 0 COMMENT '本月准时到货的订单数',
`quality_passed` INT NOT NULL DEFAULT 0 COMMENT '本月质检合格的批次数',
`quality_total` INT NOT NULL DEFAULT 0 COMMENT '本月质检总批次数',
`response_hours_avg` DECIMAL(8, 2) NULL COMMENT '本月平均响应时长(小时),从发询价到回价的平均时间',
`price_comparison` DECIMAL(8, 4) NULL COMMENT '价格竞争力系数,本供应商均价/市场均价,<1表示低于市场价',
-- ==================== 各维度评分(满分各25分)====================
`delivery_score` DECIMAL(5, 2) NOT NULL DEFAULT 0.00 COMMENT '准时交货评分(满分25分)= 准时到货率 × 25',
`quality_score` DECIMAL(5, 2) NOT NULL DEFAULT 0.00 COMMENT '质量合格评分(满分25分)= 质量合格率 × 25',
`response_score` DECIMAL(5, 2) NOT NULL DEFAULT 0.00 COMMENT '响应速度评分(满分25分)= f(平均响应时长)',
`price_score` DECIMAL(5, 2) NOT NULL DEFAULT 0.00 COMMENT '价格竞争力评分(满分25分)= f(价格系数)',
-- ==================== 综合评分与评级 ====================
`total_score` DECIMAL(5, 2) NOT NULL DEFAULT 0.00 COMMENT '综合评分 = 四项评分之和,满分100',
`grade` CHAR(1) NOT NULL COMMENT '本月评级:S/A/B/C',
`grade_changed` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '评级是否发生变化:0=未变 1=已变(触发通知)',
`prev_grade` CHAR(1) NULL COMMENT '上月评级,用于对比展示',
-- ==================== 计算说明 ====================
`calc_remark` VARCHAR(512) NULL COMMENT '评分计算说明,记录本月特殊情况(如无采购单、数据不足)',
`calc_time` DATETIME NOT NULL COMMENT '定时任务执行计算的时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_supplier_month` (`supplier_id`, `score_month`) COMMENT '同一供应商同一月份只有一条记录',
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_score_month` (`score_month`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci
COMMENT = '供应商月度评分记录表(只增不改)';
2.6 供应商审核日志表 SQL
-- ============================================================
-- 供应商审核日志表
-- 记录供应商状态的每一次变更,是审核流程的完整追溯链
-- ============================================================
CREATE TABLE `supplier_audit_log`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`supplier_id` BIGINT NOT NULL COMMENT '关联供应商ID',
`from_status` TINYINT NULL COMMENT '变更前的状态值(NULL表示首次创建)',
`to_status` TINYINT NOT NULL COMMENT '变更后的状态值',
`action` VARCHAR(64) NOT NULL COMMENT '操作动作描述:如 提交审核/审核通过/审核拒绝/停用/重新启用',
`audit_remark` VARCHAR(512) NULL COMMENT '审核意见或操作备注(拒绝时必填)',
`operator_id` BIGINT NOT NULL COMMENT '操作人的用户ID',
`operator_name` VARCHAR(64) NOT NULL COMMENT '操作人姓名(冗余存储,防止用户被删后查不到)',
`operate_time` DATETIME NOT NULL COMMENT '操作发生时间',
PRIMARY KEY (`id`),
KEY `idx_supplier_id` (`supplier_id`) COMMENT '查询某供应商的全部审核历史',
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_operate_time` (`operate_time`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci
COMMENT = '供应商审核操作日志表(只增不改不删)';
2.7 数据字典补充
-- 补充供应商相关字典数据
INSERT INTO `sys_dict_type` (`id`, `dict_name`, `dict_code`, `status`, `remark`)
VALUES
(10, '供应商资质类型', 'supplier_cert_type', 1, '供应商可上传的资质文件种类'),
(11, '供应商联系人类型', 'supplier_contact_type', 1, '联系人的业务职能分类'),
(12, '公司规模', 'company_size', 1, '供应商公司的员工人数规模');
INSERT INTO `sys_dict_item` (`id`, `dict_type_id`, `dict_code`, `item_value`, `item_label`, `item_label_en`, `sort`, `css_class`, `status`)
VALUES
-- 供应商资质类型
(1001, 10, 'supplier_cert_type', '1', '营业执照', 'Business License', 1, 'primary', 1),
(1002, 10, 'supplier_cert_type', '2', '质检报告', 'Quality Report', 2, 'success', 1),
(1003, 10, 'supplier_cert_type', '3', '产品认证', 'Product Certification', 3, 'warning', 1),
(1004, 10, 'supplier_cert_type', '4', '银行账户证明', 'Bank Certificate', 4, 'info', 1),
(1005, 10, 'supplier_cert_type', '5', '其他文件', 'Other', 5, '', 1),
-- 联系人类型
(1101, 11, 'supplier_contact_type', '1', '业务对接', 'Business', 1, 'primary', 1),
(1102, 11, 'supplier_contact_type', '2', '财务对账', 'Finance', 2, 'success', 1),
(1103, 11, 'supplier_contact_type', '3', '技术支持', 'Technical', 3, 'info', 1),
(1104, 11, 'supplier_contact_type', '4', '紧急联系', 'Emergency', 4, 'danger', 1),
-- 公司规模
(1201, 12, 'company_size', '1', '50人以下', '<50', 1, '', 1),
(1202, 12, 'company_size', '2', '50-200人', '50-200', 2, '', 1),
(1203, 12, 'company_size', '3', '200-500人', '200-500', 3, '', 1),
(1204, 12, 'company_size', '4', '500人以上', '>500', 4, '', 1);
第三节 供应商信息管理(CRUD)
3.1 CRUD 是什么
CRUD 是数据管理中最基础的四个操作,几乎所有业务功能都建立在 CRUD 之上。
| 字母 | 操作 | HTTP 方法 | 业务含义 |
|---|---|---|---|
| C | Create(创建) | POST | 新增一个供应商 |
| R | Read(读取) | GET | 查询供应商列表/查询某个供应商详情 |
| U | Update(更新) | PUT | 编辑供应商信息 |
| D | Delete(删除) | DELETE | 删除供应商(逻辑删除) |
3.2 供应商编码自动生成规则
供应商编码(supplier_code)需要系统自动生成,规则如下:
格式:SUP-{YYYYMMDD}-{4位序号}
示例:SUP-20250117-0001
生成逻辑:
1. 取当前日期,格式化为 YYYYMMDD
2. 查询当天已创建的供应商数量,+1 作为序号,不足4位补零
3. 拼接完整编码:SUP-20250117-0001
4. 检查编码唯一性(并发场景下同一时刻可能生成相同编码)
5. 唯一性冲突时重试,直到生成唯一编码
flowchart TD
A["调用生成供应商编码方法"] --> B["获取当前日期<br>format: YYYYMMDD"]
B --> C["查询数据库<br>当天已存在的最大序号"]
C --> D["序号 = 已有最大序号 + 1<br>如:当天已有3条,则序号=0004"]
D --> E["拼接编码<br>SUP-20250117-0004"]
E --> F["检查编码是否已存在<br>SELECT COUNT 验证"]
F --> G{"是否重复?"}
G -- "不重复(正常情况)" --> H["返回编码<br>SUP-20250117-0004"]
G -- "重复(并发冲突)" --> I["序号 +1 重新生成"]
I --> E
优化做法:
- Redis INCR 原子自增,天然防并发重复。
- Key 设置过期时间,比如 2-3 天。
- 数据库仍然保留唯一索引
uk_tenant_supplier_code兜底。 - 如果 Redis 不可用,可以降级到数据库序列表或数据库唯一冲突重试。
我们可以先在Redis中进行查询 当天的Key是否存在 如果存在 直接调用 INCR 自增 获取最新的编号 如果不存在 要先查询数据库 看是否存在当天的供应商的编码 如果不存在说明 当天还没有新增过 先往Redis中 新增一个Key 默认值是 0 然后执行 INCR 1
如果存在说明 当天已经新增过 但是Redis的Key已经丢失了 找到当天最大的那个值 然后存储到Redis中
然后拿到 INCR 后的值 再做拼接
private String nextAvailableCode(Long tenantId, String prefix, long sequence) {
long currentSequence = sequence;
while (true) {
String code = prefix + String.format("%04d", currentSequence);
Long count = supplierMapper.selectCount(Wrappers.<Supplier>lambdaQuery()
.eq(Supplier::getTenantId, tenantId)
.eq(Supplier::getSupplierCode, code));
if (count == null || count == 0) {
return code;
}
currentSequence++;
}
}
3.3 供应商列表查询业务规则
每个卖家 只能查询自己引入/签约的供应商 要自动拼接 租户ID
列表查询需要支持多维度组合筛选,这是使用最频繁的功能:
flowchart TD
A["前端发起列表查询请求"] --> B["传入筛选参数"]
B --> B1["供应商名称(模糊搜索)"]
B --> B2["供应商类型(精确匹配)"]
B --> B3["当前状态(精确匹配)"]
B --> B4["综合评级(精确匹配)"]
B --> B5["创建时间范围(区间查询)"]
B --> B6["页码 page 和每页数 size"]
B1 & B2 & B3 & B4 & B5 & B6 --> C["构建动态查询条件<br>有值才拼 WHERE 条件<br>没值则忽略该条件"]
C --> D["多租户插件自动追加<br>AND tenant_id = 当前租户ID"]
D --> E["分页插件自动处理<br>LIMIT offset, size<br>和 COUNT(*) 查询"]
E --> F["按创建时间倒序排列<br>ORDER BY create_time DESC"]
F --> G["返回分页结果<br>total总数 + records列表数据"]
列表接口返回数据示例(前端需要的字段):
{
"code": 200,
"msg": "success",
"data": {
"total": 156,
"pages": 8,
"current": 1,
"size": 20,
"records": [
{
"id": "1748291234567890",
"supplierCode": "SUP-20250117-0001",
"supplierName": "广州市XX电子科技有限公司",
"supplierTypeName": "工厂供应商",
"contactName": "张经理",
"contactPhone": "138****8888",
"province": "广东省",
"city": "广州市",
"grade": "A",
"score": 88.50,
"status": 2,
"statusName": "已通过",
"moq": 100,
"leadTimeDays": 7,
"createTime": "2025-01-17 10:30:00",
"certExpireWarning": true
}
]
}
}
注意:手机号在列表中需要脱敏显示(
138****8888),完整手机号只在详情页有权限的用户才能查看。certExpireWarning: true表示该供应商有资质文件即将到期,前端需要用醒目颜色标注。
有的角色只能访问列表 不能访问详情页 有的角色可以访问任何页面 有的角色不能看到完整的手机号码 有的角色是可以看到完整的手机号码
这个有权限来进行控制的 能否进行接口的调用 比如: 当前这个列表查询的接口 只要是有权限调用这个接口的角色 看到的都是 脱敏后的数据 如果想要看到完整的数据 就需要查看详情 但是查看详情的时候 会要求相关权限
3.4 供应商详情查询
详情查询要同时返回主表信息 + 关联的资质文件 + 联系人列表 + 最近评分记录:
flowchart LR
A["GET /supplier/{id}"] --> B["查询 supplier 主表<br>获取基础信息"]
B --> C["并行查询关联数据"]
C --> D["查询 supplier_cert<br>获取全部资质文件"]
C --> E["查询 supplier_contact<br>获取联系人列表"]
C --> F["查询 supplier_score_log<br>获取最近12个月评分"]
C --> G["查询 supplier_audit_log<br>获取审核操作历史"]
D & E & F & G --> H["组装返回数据<br>VO对象"]
H --> I["返回完整供应商详情"]
优化操作: 如果串行查询是这样:
查主表 20ms
查资质 30ms
查联系人 20ms
查评分 40ms
查审核日志 30ms
总耗时约 140ms
并行查询是这样:
查主表 20ms
然后同时查资质、联系人、评分、审核日志
总耗时约 20ms + max(30,20,40,30) = 60ms
可以使用CompletableFuture或线程池来实现
@Override
public SupplierDetailVO detail(Long id) {
// 校验 ID
if (ObjectUtil.isNull(id) || id <= 0) BusinessException.throwException("参数非法");
// 查询主表
Supplier supplier = supplierMapper.selectById(id);
if (ObjectUtil.isNull(supplier)) {
throwSupplierException(SupplierErrorCode.SUPPLIER_NOT_FOUND);
}
// 在主表查询后 拿到主表的ID 然后开始异步查询每个关联表的数据
// certFuture 结果集 可能是一个集合 也有可能是异常 CompletableFuture<List<SupplierCert>> certFuture = asyncQuery("供应商资质", () -> certMapper.selectList(
new LambdaQueryWrapper<SupplierCert>().eq(SupplierCert::getSupplierId, id)));
CompletableFuture<List<SupplierContact>> contactFuture = asyncQuery("供应商联系人", () -> contactMapper.selectList(
new LambdaQueryWrapper<SupplierContact>().eq(SupplierContact::getSupplierId, id)));
CompletableFuture<List<SupplierScoreLog>> scoreFuture = asyncQuery("供应商评分", () -> scoreLogMapper.selectList(
new LambdaQueryWrapper<SupplierScoreLog>()
.eq(SupplierScoreLog::getSupplierId, id)
.orderByDesc(SupplierScoreLog::getScoreMonth)
.last("LIMIT 12")));
CompletableFuture<List<SupplierAuditLog>> auditFuture = asyncQuery("供应商审核日志", () -> auditLogMapper.selectList(
new LambdaQueryWrapper<SupplierAuditLog>()
.eq(SupplierAuditLog::getSupplierId, id)
.orderByDesc(SupplierAuditLog::getOperateTime)));
try {
// join() 阻塞操作 要在这个地方 等待上面的所有的异步都执行完成
CompletableFuture.allOf(certFuture, contactFuture, scoreFuture, auditFuture).join();
} catch (CompletionException exception) {
log.error("供应商详情关联数据查询失败,supplierId={}", id, exception);
BusinessException.throwException("供应商关联数据查询失败");
}
// 主表 以及 4个 关联表 都已经查询完成
SupplierDetailVO detail = BeanUtil.copyProperties(supplier, SupplierDetailVO.class);
detail.setCerts(certFuture.join());
detail.setContacts(contactFuture.join());
detail.setScoreLogs(scoreFuture.join());
detail.setAuditLogs(auditFuture.join());
return detail;
}
private <T> CompletableFuture<List<T>> asyncQuery(String queryName, SupplierRelationQuery<T> query) {
return CompletableFuture.supplyAsync(() -> {
try {
return query.query();
} catch (RuntimeException exception) {
log.error("{}查询失败", queryName, exception);
throw exception;
}
}, supplierTaskExecutor);
}
3.5 新增供应商业务校验规则
新增供应商 是怎么触发 ?? 有卖家的采购员 去谈 供应商 采购员就可以在卖家的后台界面录入供应商的基本信息 然后提交保存
所有的前端校验 都可以通过某些手段 进行绕过 也就是说 都不靠谱 所以 不管前端是否有校验 后端都必须要有校验 只要数据库中的数据 一旦出现了问题 就是你的锅
新增供应商时,后端必须做严格的数据校验,不能完全依赖前端:
数据库表 分析 不能完全依赖这个字段约束 还要看 表关系 更主要的是 业务的上下文 业务文档 分析 业务的上下文
| 字段 | 校验规则 | 错误提示 |
|---|---|---|
supplierName | 必填,长度 2-128 字符 | ”供应商名称不能为空” |
supplierType | 必填,值必须在 [1,2,3] 内 | ”供应商类型不合法” |
contactPhone | 必填,符合手机号格式(11位数字,1开头) | “手机号格式不正确” |
contactEmail | 必填,符合邮箱格式 | ”邮箱格式不正确” |
moq | 选填,若填写必须 > 0 | ”最小起订量必须大于0” |
leadTimeDays | 选填,若填写必须在 1-365 之间 | ”交货周期必须在1-365天之间” |
supplierName | 同一租户内供应商名称不允许重复 | ”供应商名称已存在” |
contactEmail | 同一租户内邮箱不允许重复(用于Portal登录) | “该邮箱已被使用” |
同一个供应商 可以给不同的租户 进行合作 但是每个租户下 都要新增这个供应商的信息 不是按供应商记录的 而是按租户进行记录的
3.6 编辑供应商业务规则
采购员提交供应商信息后 默认的状态是 草稿 如果是草稿 就可以反复修改 一旦提交了审核 在审核完成前 是不可以修改
前端提交编辑请求 这个也是一个接口 根据ID查询一个供应商的信息
flowchart TD
A["前端提交编辑请求"] --> B["校验供应商ID是否存在"]
B --> C{"供应商当前状态?"}
C -- "草稿 / 已拒绝" --> D["允许编辑全部字段"]
C -- "待审核" --> E["❌ 禁止编辑<br>提示:审核进行中,请等待审核结果"]
C -- "已通过 / 已停用" --> F["只允许编辑部分字段<br>联系人/备注/标签<br>核心财务信息需要重新审核"]
D --> G["校验新数据合法性"]
F --> G
G --> H["更新数据库<br>version字段+1(乐观锁)"]
H --> I["记录操作审计日志"]
I --> J["返回成功"]
E --> K["返回错误码 10002<br>操作不允许"]
优化操作: 乐观锁 CAS更新
// 构建 新的 实体类
Supplier update = buildUpdateSupplier(existing, request);
// CAS 更新
update.setVersion(request.getVersion() + 1);
int rows = supplierMapper.update(update, new LambdaUpdateWrapper<Supplier>()
.eq(Supplier::getId, id)
.eq(Supplier::getVersion, request.getVersion())); // 对比当前的版本号 CAS
if (rows == 0) {
BusinessException.throwException(ResultCode.DATA_VERSION_CONFLICT);
}
3.7 删除供应商业务规则
删除是风险最高的操作,必须做关联检查:
flowchart TD
A["前端发起删除请求"] --> B["检查该供应商是否有<br>未完成的采购订单"]
B --> C{"存在未完成采购单?"}
C -- 是 --> D["❌ 禁止删除<br>提示:该供应商有X笔进行中的采购单<br>无法删除"]
C -- 否 --> E["检查供应商当前状态"]
E --> F{"状态是否为已通过?"}
F -- 是 --> G["提示确认:正式合作供应商删除后<br>历史采购记录仍保留,确认继续?"]
G --> H{"用户确认"}
H -- 确认 --> I["执行逻辑删除<br>is_deleted = 1"]
H -- 取消 --> J["取消操作"]
F -- "否(草稿/拒绝/停用)" --> I
I --> K["Portal账号同步禁用"]
K --> L["记录审计日志"]
L --> M["返回成功"]
D --> N["返回错误信息"]
业务操作: 事务控制 + 逻辑删除(本质是UPDATE操作)
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
Supplier existing = requireSupplier(id);
// 审核中的 供应商 不可以直接删除
// 如果有 订货单 还没有结束 不允许删除 当前还未实现 if (SupplierStatus.PENDING_AUDIT.getCode().equals(existing.getStatus())) {
throwSupplierStatusException("审核进行中,请等待审核结果");
}
Supplier update = new Supplier();
update.setPortalEnabled(0);
// 逻辑删除
update.setIsDeleted(1);
int rows = supplierMapper.update(update, new LambdaUpdateWrapper<Supplier>().eq(Supplier::getId, id));
if (rows == 0) {
throwSupplierException(SupplierErrorCode.SUPPLIER_NOT_FOUND);
}
// 记录操作日志
insertAuditLog(id, existing.getStatus(), existing.getStatus(), DELETE_ACTION, "逻辑删除供应商");
}
第四节 供应商审核工作流
4.1 状态机深度解析
**状态机是企业项目中最重要的设计模式之一,几乎所有业务单据(采购单/订单/退款单)都用状态机管理。
什么是状态机?
状态机描述一个对象在其生命周期中:
- 可能处于哪些状态(State)
- 每个状态下可以执行哪些事件(Event)
- 执行事件后会转变到哪个新状态(Transition)
状态机 限定了 只能是哪些状态 状态机非常重要的功能 就是控制 状态之间的流转 我们要进行编码的时候要对当前的状态进行判断 当前属于什么状态 且 只能跳转到什么状态 这些都是状态机里面规定的 setStates(“ABC”);
状态机就是用于 描述 当前业务 允许哪些状态的值 状态之间的流转过程是怎么样 都是状态机来管理的 状态机是一种概念 我们通常会使用 状态机白名单 来进行管理状态之间的流转
什么是状态机白名单 从技术层面上来说 就是一个Map Key Value Key是 运行的状态 Value 从当前Key运行跳转的状态的集合 String: SET<>
状态的修改 要满足/要经过 状态机白名单
供应商状态机定义:
stateDiagram-v2
[*] --> 草稿 : 采购专员创建供应商
草稿 --> 待审核 : 提交审核
待审核 --> 已通过 : 采购负责人审核通过
待审核 --> 已拒绝 : 采购负责人审核拒绝
待审核 --> 草稿 : 采购负责人要求补充信息
已拒绝 --> 草稿 : 采购专员修改后重新提交
已通过 --> 已停用 : 采购负责人停用
已停用 --> 已通过 : 采购负责人重新启用
已通过 --> [*] : 删除(逻辑删除)
草稿 --> [*] : 删除(逻辑删除)
已拒绝 --> [*] : 删除(逻辑删除)
状态编码与含义:
| 状态码 | 状态名称 | 含义 | 可执行操作 |
|---|---|---|---|
| 0 | 草稿 | 初始创建状态,信息可能不完整 | 编辑、提交审核、删除 |
| 1 | 待审核 | 已提交,等待负责人审核 | 无(等待审核) |
| 2 | 已通过 | 正式合作供应商 | 编辑部分字段、停用、向其下采购单 |
| 3 | 已拒绝 | 审核未通过 | 编辑后重新提交、删除 |
| 4 | 已停用 | 暂停合作 | 重新启用 |
优化操作: 状态机白名单
public final class SupplierStateMachine {
private static final Set<Transition> ALLOWED_TRANSITIONS = Set.of(
transition(SupplierStatus.DRAFT, SupplierStatus.PENDING_AUDIT),
transition(SupplierStatus.PENDING_AUDIT, SupplierStatus.APPROVED),
transition(SupplierStatus.PENDING_AUDIT, SupplierStatus.REJECTED),
transition(SupplierStatus.PENDING_AUDIT, SupplierStatus.DRAFT),
transition(SupplierStatus.REJECTED, SupplierStatus.DRAFT),
transition(SupplierStatus.APPROVED, SupplierStatus.DISABLED),
transition(SupplierStatus.DISABLED, SupplierStatus.APPROVED)
);
private static final Set<Integer> DELETE_ALLOWED_STATUSES = Set.of(
SupplierStatus.DRAFT.getCode(),
SupplierStatus.APPROVED.getCode(),
SupplierStatus.REJECTED.getCode()
);
private SupplierStateMachine() {
}
/**
* 判断状态流转是否在白名单中。 * * @param fromStatus 原状态
* @param toStatus 目标状态
* @return 是否允许流转
*/ public static boolean canTransit(Integer fromStatus, Integer toStatus) {
return ALLOWED_TRANSITIONS.contains(new Transition(fromStatus, toStatus));
}
/**
* 判断当前状态是否允许逻辑删除。 * * @param status 当前状态
* @return 是否允许逻辑删除
*/ public static boolean canDelete(Integer status) {
return DELETE_ALLOWED_STATUSES.contains(status);
}
private static Transition transition(SupplierStatus fromStatus, SupplierStatus toStatus) {
return new Transition(fromStatus.getCode(), toStatus.getCode());
}
private record Transition(Integer fromStatus, Integer toStatus) {
}
}
4.2 审核流程完整实现
flowchart TD
A(["采购专员填写供应商信息"]) --> B["保存为草稿<br>status = 0"]
B --> C["继续完善信息<br>上传资质文件"]
C --> D["点击'提交审核'按钮"]
D --> E["系统校验必填项<br>是否都已填写"]
E --> F{"校验结果"}
F -- "有未填项" --> G["提示缺少字段<br>阻止提交"]
G --> C
F -- "校验通过" --> H["校验资质文件<br>营业执照是否已上传"]
H --> I{"资质是否完整?"}
I -- "不完整" --> J["提示必须上传营业执照<br>才可提交审核"]
J --> C
I -- "完整" --> K["更新状态:草稿→待审核<br>status = 1"]
K --> L["写入审核日志<br>action=提交审核"]
L --> M["发送消息通知<br>通知有审核权限的用户"]
M --> N["采购负责人查看<br>待审核供应商列表"]
N --> O["核实资质信息<br>(人工审核)"]
O --> P{"审核决定"}
P -- "通过" --> Q["更新状态:待审核→已通过<br>status = 2"]
Q --> R["自动创建Portal账号<br>发送欢迎邮件"]
R --> S["写入审核日志<br>action=审核通过"]
P -- "拒绝" --> T["填写拒绝原因(必填)"]
T --> U["更新状态:待审核→已拒绝<br>status = 3"]
U --> V["写入审核日志<br>action=审核拒绝<br>记录拒绝原因"]
V --> W["通知采购专员<br>审核结果+拒绝原因"]
P -- "要求补充" --> X["填写需补充的内容"]
X --> Y["更新状态:待审核→草稿<br>status = 0"]
Y --> Z["通知采购专员<br>需要补充的信息"]
Z --> C
S --> AA(["供应商正式上线"])
W --> AB(["等待重新提交"])
4.3 审核接口的幂等性设计
什么是幂等性? 相同的操作执行多次,结果和执行一次完全相同。
为什么需要幂等? 网络不稳定时,前端可能发送了审核通过的请求,但因为网络超时误以为失败,于是重新发送。如果接口不幂等,同一个供应商可能被审核通过两次,产生两条审核日志,或者产生两个Portal账号。
flowchart TD
A["前端发起审核通过请求<br>supplier_id = 1001"] --> B["查询供应商当前状态"]
B --> C{"当前状态是否为<br>待审核(status=1)?"}
C -- "否(已经是已通过了)" --> D["返回成功<br>提示:该供应商已审核通过<br>无需重复操作"]
C -- "是" --> E["执行审核通过逻辑<br>更新状态 + 创建账号 + 发通知"]
E --> F["返回成功"]
D --> G["前端展示最新状态"]
F --> G
优化操作:
幂等校验(在修改状态的时候 添加条件 where 状态=待审核) + 状态前置判断(不是幂等性校验) + 唯一约束兜底(数据库的唯一约束)
先判断当前供应商是否仍是“待审核”状态,如果已经通过,就直接返回成功,不重复创建 Portal 账号。
同时 portal_user_id 和用户账号也做唯一性约束,防止并发请求造成脏数据。
// 使用MP进行更新的时候 必须要传递一个Bean
Supplier update = new Supplier();
update.setStatus(SupplierStatus.PENDING_AUDIT.getCode());
int rows = supplierMapper.update(update, new LambdaUpdateWrapper<Supplier>()
.eq(Supplier::getId, id) // 根据供应商ID进行更新
.eq(Supplier::getStatus, SupplierStatus.DRAFT.getCode())); // CAS 幂等性保障
if (rows == 0) {
throwSupplierStatusException("供应商状态已变化,请刷新后重试");
}
// 插入审核日志
insertAuditLog(id, existing.getStatus(), SupplierStatus.PENDING_AUDIT.getCode(),
SupplierAuditAction.SUBMIT.getDescription(), "提交供应商审核");
4.4 审核操作的权限控制
| 操作 | 所需权限标识 | 角色要求 |
|---|---|---|
| 提交审核 | srm:supplier:submit | 采购专员及以上 |
| 审核通过 | srm:supplier:audit | 采购负责人/租户管理员 |
| 审核拒绝 | srm:supplier:audit | 采购负责人/租户管理员 |
| 停用供应商 | srm:supplier:disable | 采购负责人/租户管理员 |
| 重新启用 | srm:supplier:enable | 采购负责人/租户管理员 |
业务规则:采购专员不能审核自己提交的供应商(防止利益冲突)。系统需要判断提交人和审核人是否为同一人,若是同一人则拒绝操作。
第五节 资质文件上传模块
5.1 文件上传的业务背景
供应商资质审核需要真实可查的证明文件,常见的资质文件包括:
| 资质类型 | 文件说明 | 是否必须上传才可提交审核 |
|---|---|---|
| 营业执照 | 证明供应商是合法注册的企业 | ✅ 必须 |
| 质检报告 | 证明商品符合质量标准 | 建议(视品类而定) |
| 产品认证 | CE(欧盟)、FCC(美国)、UL(美国)等认证 | 视目的市场要求 |
| 银行账户证明 | 用于付款验证,防止汇款给错误账户 | 建议 |
5.2 文件上传整体架构
graph TD
subgraph 前端
A["用户选择文件<br>点击上传按钮"] --> B["文件格式校验<br>类型/大小/数量"]
B --> C["调用上传接口<br>multipart/form-data"]
end
subgraph 后端上传服务
D["接收文件流"] --> E["安全检查<br>文件类型白名单验证<br>防止恶意文件上传"]
E --> F["重命名文件<br>UUID+扩展名<br>防止文件名冲突和路径遍历攻击"]
F --> G{"存储类型配置"}
G -- "开发环境local" --> H["存储到本地磁盘<br>/uploads/{tenantId}/{date}/"]
G -- "生产环境oss" --> I["上传到阿里云OSS<br>返回CDN访问URL"]
H & I --> J["写入 supplier_cert 表<br>记录文件元数据"]
J --> K["返回文件URL<br>和文件记录ID"]
end
subgraph 文件访问
L["前端展示图片/预览PDF"] --> M{"文件类型"}
M -- "图片" --> N["直接img标签展示"]
M -- "PDF" --> O["iframe或新标签页打开"]
end
C --> D
K --> L
5.3 文件上传接口设计
上传接口:
POST /api/file/upload
Content-Type: multipart/form-data
请求参数:
file : 文件二进制数据(必填)
bizType : 业务类型,如:supplier_cert(必填,用于文件分类存储)
bizId : 关联的业务ID,如:供应商ID(选填)
响应结果:
{
"code": 200,
"data": {
"fileId": "1748291234567891",
"fileName": "营业执照.pdf",
"fileUrl": "https://flexchain.oss-cn-hangzhou.aliyuncs.com/supplier/1001/20250117/a3b4c5d6.pdf",
"fileSizeKb": 256,
"fileType": "application/pdf"
}
}
5.4 文件安全校验规则
flowchart TD
A["文件上传请求到达"] --> B["检查文件是否为空"]
B --> C{"文件为空?"}
C -- 是 --> D["返回错误:请选择要上传的文件"]
C -- 否 --> E["检查文件大小<br>fileSize <= 5MB = 5×1024×1024 Byte"]
E --> F{"超过5MB?"}
F -- 是 --> G["返回错误:文件大小不能超过5MB"]
F -- 否 --> H["检查文件扩展名<br>白名单:pdf/jpg/jpeg/png/doc/docx/xls/xlsx"]
H --> I{"扩展名合法?"}
I -- 否 --> J["返回错误:不支持该文件类型"]
I -- 是 --> K["检查文件真实类型<br>读取文件头部Magic Bytes<br>防止改扩展名绕过"]
K --> L{"真实类型匹配?"}
L -- "不匹配(如jpg改为pdf)" --> M["返回错误:文件内容与扩展名不符"]
L -- 匹配 --> N["生成存储路径<br>/{tenantId}/{bizType}/{YYYYMMDD}/{UUID}.{ext}"]
N --> O["执行文件存储"]
安全重点:文件类型检查不能只看扩展名!黑客可以把
.php改成.jpg绕过检查。必须读取文件的前几个字节(Magic Bytes)验证真实类型。例如:PDF 文件前4字节一定是25 50 44 46(%PDF)。
优化操作: 扩展名白名单
- MIME 类型初筛
- Magic Bytes 校验
- 文件大小限制
- 文件名重命名
- OSS 私有读写
- 必要时杀毒扫描(线上)
- 图片内容的审核(OSS自带 涉政 涉黄 暴力 恐怖) 注意:docx/xlsx 本质上是 zip 包,Magic Bytes 可能都是 zip 文件头,还需要进一步检查内部结构。
同一个模块中的多个业务 需要使用 可以抽取为 公共类/工具类 如果多个系统模块都需要使用 甚至是多个项目也都需要使用 我们可以考虑封装成一个自定义的起步依赖 自定义 starter
5.5 OSS 存储路径规范
OSS Bucket:flexchain-files
存储路径规范:/{tenantId}/{bizType}/{YYYY-MM-DD}/{UUID}.{ext}
示例:
/101/supplier_cert/2025-01-17/a3b4c5d6e7f8.pdf ← 租户101上传的供应商资质PDF
/102/product_img/2025-01-17/b4c5d6e7f8a9.jpg ← 租户102上传的商品图片JPG
路径设计理由:
1. 按租户ID隔离:/101/ 和 /102/ 下的文件互不干扰
2. 按业务类型分类:方便管理和统计存储成本
3. 按日期分散:避免单目录文件数量过多影响性能
4. UUID文件名:全局唯一,防止冲突,同时隐藏原始文件名(安全)
5.6 资质文件到期提醒定时任务
flowchart TD
A(["定时任务:每天 09:00 执行"]) --> B["查询所有未过期的资质文件<br>WHERE is_expired = 0<br>AND expire_date IS NOT NULL<br>AND is_deleted = 0"]
B --> C["遍历每条资质文件记录"]
C --> D["计算距到期天数<br>daysLeft = expire_date - today"]
D --> E{"天数判断"}
E -- "daysLeft < 0" --> F["标记为已过期<br>UPDATE is_expired = 1"]
E -- "daysLeft = 30" --> G["发送30天到期提醒<br>通知采购专员更新资质"]
E -- "daysLeft = 7" --> H["发送7天紧急提醒<br>通知采购专员和采购负责人"]
E -- "daysLeft = 1" --> I["发送明日到期紧急提醒<br>通知更多层级"]
F & G & H & I --> J["继续处理下一条"]
J --> K{"是否还有<br>待处理的记录?"}
K -- 是 --> C
K -- 否 --> L["记录任务执行日志<br>处理数量/发送通知数"]
L --> M(["定时任务结束"])
XXL-JOB

优化操作: 资质到期提醒
每天 09:00 扫描快到期证照
按 tenant_id 或 cert_id 分片
每个分片处理一部分数据
批量分页查询
线程池并发发送站内信/邮件
分片广播的用法
private List<SupplierCert> queryUnexpiredCerts(int shardIndex, int shardTotal) {
LambdaQueryWrapper<SupplierCert> wrapper = new LambdaQueryWrapper<SupplierCert>()
.eq(SupplierCert::getIsExpired, 0) // 过期状态为0 代表未过期 或者是 已过期但是还没有修改这个状态
.isNotNull(SupplierCert::getExpireDate) // 过期时间不为空
.eq(SupplierCert::getIsDeleted, 0); // 未被逻辑删除的
if (shardTotal > 1) { // 判断我们的服务器数量 是否是集群 如果是单机 就不走分片广播
// 如果是集群 至少两台服务器 才会走这个条件 // id % shardTotal = shardIndex; wrapper.apply("MOD(id, {0}) = {1}", shardTotal, shardIndex);
}
return certMapper.selectList(wrapper);
}
线程池异步处理
线程池的创建 建议: 把不同业务的线程池 都提前创建好 封装成Bean 当使用的时候 直接注入线程池即可 不建议是任务触发的方法中 每次去创建线程池
/**
* 供应商资质到期扫描异步线程池。 * * @return 异步执行器
*/@Bean("supplierCertExpireExecutor")
public ThreadPoolTaskExecutor supplierCertExpireExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("supplier-cert-expire-");
executor.initialize();
return executor;
}
/**
* 按分片扫描并处理供应商资质到期数据。 * * @param shardIndex 分片序号
* @param shardTotal 分片总数
* @return 任务执行结果
*/public SupplierCertExpireJobResult scanCertExpire(int shardIndex, int shardTotal) {
// 根据分片数量和当前的分片索引号 查询所有资质
// 按照分片的数量 进行查询资质文件 List<SupplierCert> certs = queryUnexpiredCerts(shardIndex, shardTotal);
// 扫描的结果封装Bean
SupplierCertExpireJobResult result = new SupplierCertExpireJobResult();
List<CompletableFuture<Void>> futures = certs.stream()
// CompletableFuture.runAsync 异步处理 原子性更新 也就是说多个线程同时处理扫描到的数据 也不会出现 result 的并发问题
.map(cert -> CompletableFuture.runAsync(() -> processCert(cert, result), certExpireExecutor)
// 如果出现了异常
.exceptionally(exception -> {
// 记录了异常的失败次数
// 如果失败了 需要重新 processCert 吗 ?? // 不需要 当天失败了 明天还会再处理一次 1天 处理失败了 明天就过期了 result.incrementFailed();
log.error("供应商资质到期处理失败,certId={}", cert.getId(), exception);
return null;
}))
.toList();
// 阻塞操作 等待所有的任务都执行完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 如果要处理失败 有两种做法
// 第一种是 exception 中直接使用 try catch 在 catch 中进行处理 重试 重新判断 // 第二种是 在所有的线程都执行完成后 判断失败的总的次数 > 0 如果是 就根据失败的资质的ID 针对性的去判断 // 第三种是 不处理 等着下一次 定时任务的触发 到时候再重新处理 // 如果我们要实现增量 或者 需要记录每天的任务结果 就需要创建一个MySQL的表 来存储下面的数据 // log 可以直接写入到文件中 也算是持久化了 log.info("供应商资质到期扫描完成,shardIndex={}, shardTotal={}, scanned={}, expired={}, notice={}, failed={}",
shardIndex, shardTotal, result.getScannedCount(), result.getExpiredCount(),
result.getNoticeCount(), result.getFailedCount());
return result;
}
传统的线程池 实现方式
/**
* 使用传统线程池按分片扫描并处理供应商资质到期数据。 * * @param shardIndex 分片序号
* @param shardTotal 分片总数
* @return 任务执行结果
*/public SupplierCertExpireJobResult scanCertExpire(int shardIndex, int shardTotal) {
// 根据分片数量和当前的分片索引号 查询所有资质
// 按照分片的数量 进行查询资质文件 List<SupplierCert> certs = queryUnexpiredCerts(shardIndex, shardTotal);
// 扫描的结果封装Bean
SupplierCertExpireJobResult result = new SupplierCertExpireJobResult();
List<Future<?>> futures = new ArrayList<>(certs.size());
for (SupplierCert cert : certs) {
try {
// submit 是有返回值的 拿到异常 和 结果
// get 阻塞式 futures.add(certExpireExecutor.submit(() -> processCertSafely(cert, result)));
} catch (RejectedExecutionException exception) {
result.incrementFailed();
log.error("供应商资质到期任务提交线程池失败,certId={}", cert.getId(), exception);
}
}
// 当所有的线程都执行完成后
// 怎么判断 所有的线程都已经执行完毕了 ??
// submit 可以通过 future.get(); 来阻塞主线程的执行 等待子任务执行完成 waitForTraditionalTasks(futures, result);
logScanResult(shardIndex, shardTotal, result);
return result;
}
private void waitForTraditionalTasks(List<Future<?>> futures, SupplierCertExpireJobResult result) {
for (Future<?> future : futures) {
try {
future.get();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
result.incrementFailed();
log.error("供应商资质到期线程等待被中断", exception);
return;
} catch (ExecutionException exception) {
result.incrementFailed();
log.error("供应商资质到期线程执行异常", exception);
}
}
// 这个循环走完 也就意味着 所有的子线程都已经执行完毕了
}
第六节 供应商绩效评分系统
6.1 为什么需要量化评分
纯靠感觉选供应商 → 容易受个人关系影响,无法公平对比 量化评分体系 → 数据说话,客观公平,可追溯
评分维度与权重设计(满分100分):
| 评分维度 | 权重 | 满分 | 计算数据来源 |
|---|---|---|---|
| 准时交货率 | 25% | 25分 | 采购订单的实际到货日期 vs 承诺到货日期 |
| 质量合格率 | 25% | 25分 | 入库质检的合格批次数 / 总批次数 |
| 响应速度 | 25% | 25分 | 收到询价到回复报价的平均小时数 |
| 价格竞争力 | 25% | 25分 | 本供应商均价 / 同品类市场均价 |
6.2 各维度评分计算细则
① 准时交货率评分(满分25分)
准时交货率 = 当月准时到货的采购单数 / 当月全部到货的采购单数 × 100%
准时定义:实际到货日期 ≤ 采购单中的承诺到货日期
评分公式:准时交货评分 = 准时交货率 × 25
示例:
本月共10笔采购单到货,其中8笔准时到货
准时交货率 = 8/10 = 80%
准时交货评分 = 80% × 25 = 20分
② 质量合格率评分(满分25分)
质量合格率 = 当月质检合格批次数 / 当月质检总批次数 × 100%
评分公式:质量合格评分 = 质量合格率 × 25
示例:
本月5批货物入库质检,4批全部合格,1批部分不合格
质量合格率 = 4/5 = 80%
质量合格评分 = 80% × 25 = 20分
③ 响应速度评分(满分25分)
响应速度使用分段函数计算:
平均响应时长(小时)→ 响应速度评分
≤ 2小时 → 25分(优秀:当天内快速回价)
2-8小时 → 20分(良好:工作日内回价)
8-24小时 → 15分(一般:次日回价)
24-48小时 → 10分(较差:两天内回价)
48-72小时 → 5分(很差:三天内回价)
> 72小时 → 0分(无法接受:三天以上)
④ 价格竞争力评分(满分25分)
价格系数 = 本供应商当月采购均价 / 同品类所有供应商均价
评分公式:
价格系数 < 0.90 → 25分(比市场均价低10%以上,极具竞争力)
价格系数 0.90-0.95 → 22分
价格系数 0.95-1.00 → 18分(接近市场均价)
价格系数 1.00-1.05 → 12分(略高于市场均价)
价格系数 1.05-1.10 → 6分(高于市场均价)
价格系数 > 1.10 → 0分(明显高于市场均价)
6.3 评分计算流程
flowchart TD
A(["定时任务:每月1日 00:30 执行"]) --> B["获取上月的年月<br>如:执行日期2025-02-01<br>则计算2025-01月的数据"]
B --> C["查询本租户所有状态为<br>已通过的供应商列表"]
C --> D["遍历每个供应商"]
D --> E["查询上月该供应商的<br>采购订单到货数据"]
E --> F["计算准时交货率<br>→ 准时交货评分"]
F --> G["查询上月该供应商的<br>入库质检数据"]
G --> H["计算质量合格率<br>→ 质量合格评分"]
H --> I["查询上月该供应商的<br>询价响应记录"]
I --> J["计算平均响应时长<br>→ 响应速度评分"]
J --> K["查询上月该供应商的<br>采购价格数据"]
K --> L["计算价格竞争力系数<br>→ 价格竞争力评分"]
L --> M["汇总四项评分<br>total_score = sum"]
M --> N{"数据是否充足?<br>(本月是否有采购单)"}
N -- "无采购单" --> O["写入评分记录<br>remark=本月无采购数据<br>评分沿用上月"]
N -- "有采购单" --> P["计算综合评级<br>≥90=S ≥75=A ≥60=B <60=C"]
P --> Q["比较本月评级<br>与上月评级是否变化"]
Q --> R["写入 supplier_score_log"]
R --> S["更新 supplier 主表<br>grade 和 score 字段"]
S --> T{"评级是否变化?"}
T -- 是 --> U["发送评级变化通知<br>告知采购专员和供应商"]
T -- 否 --> V["继续下一个供应商"]
U --> V
O --> V
V --> W{"是否还有<br>未处理的供应商?"}
W -- 是 --> D
W -- 否 --> X["生成月度供应商<br>绩效汇总报告"]
X --> Y(["定时任务结束"])
6.4 评分数据的特殊情况处理
| 特殊情况 | 处理方式 |
|---|---|
| 本月没有任何采购单 | 不更新评分,保留上月评分,在 calc_remark 中注明 |
| 没有询价记录(不走询价流程直接下单) | 响应速度评分默认给满分25分 |
| 没有质检记录(100%通过无需记录) | 质量合格评分默认给满分25分 |
| 新供应商(历史数据不足1个月) | 不参与评分,等待下月数据充足后计算 |
| 供应商当月被停用 | 仍然计算当月有效数据,但下月开始不再计算 |
优化操作: 月度评分计算
每月 1 日 00:30 执行
按 tenant_id 或 supplier_id 分片
每个分片计算部分供应商评分
写入 supplier_score_log
更新 supplier 主表 grade/score
任务必须幂等,不能重复发送多次提醒。
可以加一张通知记录表防重复:supplier_cert_remind_log(cert_id, remind_type, remind_date)
第七节 供应商分层分级体系
7.1 分级与差异化合作策略
供应商评级不只是一个数字,背后对应不同的合作策略:
graph TD
subgraph S级 优质供应商
S1["评分:90-100分"]
S2["策略:战略合作<br>优先下单,增加份额<br>给予更优付款条件<br>共同开发新品"]
end
subgraph A级 良好供应商
A1["评分:75-89分"]
A2["策略:正常合作<br>日常采购首选<br>维持当前合作规模"]
end
subgraph B级 待改进供应商
B1["评分:60-74分"]
B2["策略:改进警示<br>发送改进通知书<br>列入重点观察名单<br>减少30%采购份额"]
end
subgraph C级 预警供应商
C1["评分:60分以下"]
C2["策略:预警处置<br>暂停新采购单<br>启动替代供应商<br>连续3个月C级则停用"]
end
C 级暂停采购、连续 3 个月停用,不应该是 SaaS 平台统一强制。应该是卖家自己的采购策略。 可设计成“系统建议 + 租户可配置”
默认:风险提示,不强制拦截
严格模式:创建新采购单时要求主管审批
强管控模式:禁止普通采购员下单,只允许管理员特批
如果卖家坚持使用 C 级供应商,系统可以允许,但要:
- 弹出风险提示
- 要求填写继续采购原因
- 走采购主管审批
- 写入审计日志
B级 待改进供应商
系统自动生成改进通知草稿
采购负责人确认后发送
观察名单不只是为了“连续 3 个月 C 级停用”,还可以用于:
- 采购单审批提醒
- 供应商列表标红
- 下单时风险提示
- 月度复盘
- 解除观察流程
“减少 30% 采购份额”也不应默认硬限制。 更合理的做法是系统在采购分配、补货建议、供应商推荐时降低推荐权重。是否真的少采购,由卖家采购负责人决定。
之前我们已经实现了一个功能
给每个供应商 进行评分/评级
接下来我们要根据这个评分/评级进行对供应商的归类
如果某个卖家 对供应商的要求比较高 可以自己设定这个供应商的最低的级别
如果供应商没有达到这个级别 我们要帮卖家把这个供应商的信息 保存到 重点观察名单的这个表中
当该卖家再次对这个供应商进行采购的时候 我们要查询这个 重点观察名单表 如果该供应商没有达到卖家的要求
我们就要给卖家相关的提示
哪些供应商需要划到这个表中 哪些供应商不需要划到这个表中 有谁决定 ?? 卖家 所以我们还需要提供一个配置表 我作为卖家 我能允许的最低的等级 B 你作为卖家 你能允许的最低的等级 C
遍历每个租户下的所以的供应商 然后判断当前的供应商 是否低于 卖家自己设定的等级 如果不符合 低于 把这个供应商加入到 重点观察名单表中
7.2 多供应商策略(柔性的核心)
柔性供应链要求对同一品类维护至少 2 家以上供应商,避免单点故障:
graph LR
A["同一品类:蓝牙耳机"] --> B["主供应商<br>广州S级工厂A<br>份额60%<br>平时稳定供货"]
A --> C["备用供应商<br>深圳A级工厂B<br>份额30%<br>分散风险备用"]
A --> D["应急供应商<br>义乌贸易商C<br>份额10%<br>紧急时快速补货"]
E["正常情况"] --> B
F["工厂A产能不足<br>或质量问题"] --> C
G["紧急补货<br>需要3天到货"] --> D
品类-供应商矩阵管理:
在实际业务中,采购负责人需要查看每个品类下有哪些可用供应商及其评级:
| 品类 | S级供应商 | A级供应商 | B级供应商 | 风险评估 |
|---|---|---|---|---|
| 蓝牙耳机 | 1家 | 2家 | 0家 | 低风险 ✅ |
| 手机支架 | 0家 | 1家 | 1家 | 中风险 ⚠️ |
| 数据线 | 0家 | 0家 | 2家 | 高风险 ❌ |
系统预警:某品类只有1家供应商,或所有供应商评级低于B时,系统应自动发出风险预警,提醒采购负责人开发新供应商。
某品类供应商不足,如何自动风险预警
可以由定时任务或评分任务后触发。
例如每天凌晨扫描:
按租户 + 品类统计可用供应商数量 如果可用供应商数量 < 2 或者所有供应商评级 < B 生成风险事件 发送通知
通知方式:
- 站内信:默认必须有
- 邮件:重要预警建议有
- 短信/企微/钉钉:紧急级别或企业版可选
接收人:
采购专员 采购负责人 租户管理员
建议可以建一张风险事件表,避免每天重复发同一个预警:
supplier_risk_event
每个租户都可以按照自己的要求去配置 风险 比如: 同一个类别的供应商 至少有几个 供应商的等级 最低是什么 我们的系统就可以定时去扫描 判断当前租户的供应商 是否符合 租户的配置 如果符合了 就说明是没有风险的 如果不符合 就说明是有风险的
评分.评级
第八节 供应商 Portal 协同体系
8.1 为什么需要供应商 Portal
没有Portal之前:
- 采购单通过微信/邮件发给供应商 → 容易丢失,无法追溯
- 供应商发货信息通过聊天记录传递 → 无法自动进入系统
- 对账靠人工核对Excel → 容易出错
有了供应商Portal之后:
- 供应商登录系统直接查看采购单 → 信息准确,有记录
- 供应商在线填写物流单号 → 自动触发TMS创建运单
- 在线生成对账单 → 双方实时确认,减少争议
8.2 Portal 账号创建流程
flowchart TD
A["供应商审核状态变为<br>已通过(status=2)"] --> B["触发Portal账号创建逻辑"]
B --> C["检查是否已创建过Portal账号<br>portal_user_id IS NULL"]
C --> D{"是否已有账号?"}
D -- "已有账号" --> E["跳过创建<br>直接发送通知(补发)"]
D -- "没有账号" --> F["生成登录账号<br>用户名:供应商邮箱地址<br>如 supplier艾特abc.com"]
F --> G["生成随机初始密码<br>格式:Flex + 6位随机数字<br>如 Flex123456"]
G --> H["BCrypt加密密码<br>写入 sys_user 表<br>user_type = 3(供应商用户)"]
H --> I["分配供应商角色<br>ROLE_SUPPLIER"]
I --> J["更新 supplier 表<br>portal_user_id = 新创建的user_id<br>portal_enabled = 1"]
J --> K["发送欢迎邮件<br>内容:登录地址+账号+初始密码"]
K --> L["发送Portal开通通知<br>(站内信给采购专员)"]
L --> M(["Portal账号创建完成"])
E --> K
同一个供应商给多个卖家供货,Portal 账号怎么建
按当前 V1 项目定位:多租户隔离,每个卖家维护自己的供应商档案,因此每个租户下创建独立 Portal 账号。
也就是说,同一个真实工厂:
卖家 A:supplier_id = 1001,portal_user_id = 501
卖家 B:supplier_id = 2001,portal_user_id = 801
哪怕邮箱相同,也建议账号在租户维度隔离。登录时可以用:tenant_code + email
或者要求供应商选择进入哪个卖家的 Portal。
如果做成“一个供应商账号服务多个卖家”,那就进入平台级供应商身份体系了,需要:
- 平台供应商主数据
- 供应商和租户关系表
- 一个账号绑定多个卖家
- 登录后切换合作卖家
- 不同卖家的报价、账期、评分隔离
这个功能可以放到 V2 实现
8.3 Portal 权限范围设计
Portal 是供应商视角的系统,权限极其受限,必须严格隔离:
graph TD
A["供应商Portal用户登录"] --> B{"数据访问范围"}
B --> C["✅ 可以查看<br>发给本供应商的采购订单"]
B --> D["✅ 可以填写<br>物流单号和预计发货时间"]
B --> E["✅ 可以上传<br>发货单据和快递面单照片"]
B --> F["✅ 可以查看<br>本供应商的应收账款记录"]
B --> G["✅ 可以修改<br>自己的联系方式和密码"]
B --> H["❌ 禁止查看<br>其他供应商的任何信息"]
B --> I["❌ 禁止查看<br>我方的库存和销售数据"]
B --> J["❌ 禁止查看<br>我方的财务利润数据"]
B --> K["❌ 禁止操作<br>任何会改变采购单状态的操作"]
Portal 特殊隔离机制:
供应商用户除了 tenant_id 隔离外,还需要额外的 supplier_id 隔离:
-- 供应商查询采购单时,SQL 中必须同时满足以下两个条件
-- 1. 多租户隔离(MyBatis-Plus自动追加)
WHERE tenant_id = 101
-- 2. 供应商隔离(业务代码手动追加,不能依赖自动插件)
AND supplier_id = 当前登录供应商的supplier_id
8.4 Portal 核心功能页面
| 页面 | 功能 | 操作权限 |
|---|---|---|
| 采购单列表 | 查看发给本供应商的所有采购单,含状态和金额 | 只读 |
| 采购单详情 | 查看采购单明细(品名/规格/数量/单价) | 只读 |
| 确认采购单 | 确认接单,填写预计发货时间 | 可写 |
| 填写物流信息 | 填写物流公司和运单号 | 可写 |
| 上传发货单据 | 上传装箱单、出库单等 PDF/图片 | 可写 |
| 应收账款 | 查看未付/已付账款金额和付款状态 | 只读 |
| 个人信息 | 修改手机号、邮箱、密码 | 可写 |
第九节 消息通知机制
9.1 通知场景总览
SRM 模块涉及多个关键节点需要发送通知,确保相关人员及时知晓:
| 触发事件 | 通知对象 | 通知方式 | 优先级 |
|---|---|---|---|
| 供应商提交审核 | 所有有审核权限的用户 | 站内信 | 普通 |
| 审核通过 | 提交人 + 供应商(邮件) | 站内信 + 邮件 | 普通 |
| 审核拒绝 | 提交人 | 站内信 + 邮件 | 普通 |
| 需要补充资料 | 提交人 | 站内信 | 普通 |
| 资质文件30天到期 | 采购专员 | 站内信 + 邮件 | 普通 |
| 资质文件7天到期 | 采购专员 + 采购负责人 | 站内信 + 邮件 | 紧急 |
| 供应商评级下降(如A→B) | 采购专员 + 采购负责人 | 站内信 | 普通 |
| 供应商评级进入C级预警 | 采购专员 + 采购负责人 + 租户管理员 | 站内信 + 邮件 | 紧急 |
| Portal账号开通 | 供应商(邮件) | 邮件 | 普通 |
9.2 站内信设计
flowchart LR
A["业务事件触发<br>如:审核通过"] --> B["构建通知消息体<br>标题/内容/跳转链接/接收人"]
B --> C["写入消息表<br>sys_message"]
C --> D["前端轮询或WebSocket推送<br>用户看到消息红点"]
D --> E["用户点击消息<br>标记为已读"]
E --> F["跳转到相关页面<br>如:供应商详情页"]
站内信数据表:
-- ============================================================
-- 站内消息通知表
-- 系统内部的消息通知,显示在右上角消息铃铛
-- ============================================================
CREATE TABLE `sys_message`
(
`id` BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`receiver_id` BIGINT NOT NULL COMMENT '接收人的用户ID',
`sender_id` BIGINT NULL COMMENT '发送人的用户ID,NULL表示系统自动发送',
`msg_type` VARCHAR(64) NOT NULL COMMENT '消息类型:SUPPLIER_AUDIT/CERT_EXPIRE/SCORE_CHANGE等',
`title` VARCHAR(128) NOT NULL COMMENT '消息标题,如:【供应商审核】广州工厂A已通过审核',
`content` TEXT NOT NULL COMMENT '消息详细内容,支持HTML格式',
`link_url` VARCHAR(256) NULL COMMENT '点击消息后跳转的页面路径,如:/srm/supplier/1001',
`ref_id` BIGINT NULL COMMENT '关联的业务ID,如供应商ID(用于跳转和去重)',
`ref_type` VARCHAR(32) NULL COMMENT '关联业务类型,如:SUPPLIER',
`priority` TINYINT NOT NULL DEFAULT 1 COMMENT '优先级:1=普通 2=重要 3=紧急(紧急消息弹窗提醒)',
`is_read` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已读:0=未读 1=已读',
`read_time` DATETIME NULL COMMENT '用户读取时间',
`send_time` DATETIME NOT NULL COMMENT '消息发送时间',
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除(用户手动删除消息)',
PRIMARY KEY (`id`),
KEY `idx_receiver_read` (`receiver_id`, `is_read`) COMMENT '查询用户未读消息数',
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_send_time` (`send_time`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci
COMMENT = '站内消息通知表';
9.3 邮件通知设计
邮件发送流程:
flowchart TD
A["业务触发邮件发送需求"] --> B["构建邮件内容<br>读取邮件模板<br>替换模板变量"]
B --> C["写入邮件发送队列表<br>email_send_queue<br>status=待发送"]
C --> D["定时任务每分钟扫描队列<br>取出待发送的邮件"]
D --> E["调用JavaMail<br>通过SMTP发送邮件"]
E --> F{"发送结果"}
F -- 成功 --> G["更新队列记录<br>status=已发送<br>记录发送时间"]
F -- "失败(如网络超时)" --> H["更新重试次数<br>retry_count + 1"]
H --> I{"重试次数<br>是否超过3次?"}
I -- 否 --> J["等待下次任务<br>重新发送"]
I -- 是 --> K["标记为发送失败<br>status=失败<br>记录失败原因<br>告警通知管理员"]
J --> D
邮件模板示例(供应商审核通过):
主题:【FlexChain】您的供应商申请已审核通过
邮件正文:
尊敬的 {供应商联系人姓名},
您好!
您在 FlexChain 平台的供应商注册申请已于 {审核时间} 审核通过。
您的供应商信息如下:
- 供应商名称:{供应商名称}
- 供应商编码:{供应商编码}
- 评级初始值:C级(将根据月度绩效动态调整)
您的 Portal 登录信息如下:
- 登录地址:https://portal.flexchain.com
- 登录账号:{联系人邮箱}
- 初始密码:{初始密码}
请在首次登录后及时修改密码。
如有任何问题,请联系您的采购对接人:{采购专员姓名}({采购专员手机号})
FlexChain 平台团队
第十节 接口设计规范
10.1 供应商模块完整接口清单
按照 RESTful 规范设计接口,接口路径用名词复数,HTTP 方法表达操作语义。
基础路径: /api/srm/suppliers
| 接口名称 | HTTP方法 | 路径 | 权限标识 | 说明 |
|---|---|---|---|---|
| 供应商分页列表 | GET | /api/srm/suppliers | srm:supplier:list | 支持多条件筛选 |
| 供应商详情 | GET | /api/srm/suppliers/{id} | srm:supplier:list | 含资质/联系人/评分 |
| 新增供应商 | POST | /api/srm/suppliers | srm:supplier:add | 创建草稿状态 |
| 编辑供应商 | PUT | /api/srm/suppliers/{id} | srm:supplier:edit | 含乐观锁version字段 |
| 删除供应商 | DELETE | /api/srm/suppliers/{id} | srm:supplier:delete | 软删除,需关联检查 |
| 提交审核 | PUT | /api/srm/suppliers/{id}/submit | srm:supplier:submit | 草稿→待审核 |
| 审核通过 | PUT | /api/srm/suppliers/{id}/approve | srm:supplier:audit | 待审核→已通过 |
| 审核拒绝 | PUT | /api/srm/suppliers/{id}/reject | srm:supplier:audit | 待审核→已拒绝,需填原因 |
| 停用供应商 | PUT | /api/srm/suppliers/{id}/disable | srm:supplier:disable | 已通过→已停用 |
| 重新启用 | PUT | /api/srm/suppliers/{id}/enable | srm:supplier:enable | 已停用→已通过 |
| 审核日志 | GET | /api/srm/suppliers/{id}/audit-logs | srm:supplier:list | 查询审核历史 |
| 评分历史 | GET | /api/srm/suppliers/{id}/scores | srm:supplier:list | 查询月度评分趋势 |
| 上传资质文件 | POST | /api/srm/suppliers/{id}/certs | srm:supplier:edit | 上传资质文件 |
| 删除资质文件 | DELETE | /api/srm/suppliers/{id}/certs/{certId} | srm:supplier:edit | 软删除资质记录 |
| 新增联系人 | POST | /api/srm/suppliers/{id}/contacts | srm:supplier:edit | 新增联系人 |
| 删除联系人 | DELETE | /api/srm/suppliers/{id}/contacts/{contactId} | srm:supplier:edit | 删除联系人 |
| 供应商下拉选项 | GET | /api/srm/suppliers/options | srm:supplier:list | 已通过的供应商简要信息 |
10.2 关键接口请求/响应示例
① 新增供应商 - 请求体(Request Body):
POST /api/srm/suppliers
Content-Type: application/json
Authorization: Bearer {token}
{
"supplierName": "广州市XX电子科技有限公司",
"supplierType": 1,
"categoryIds": [3, 7, 12],
"province": "广东省",
"city": "广州市",
"address": "天河区科技园A栋5楼",
"contactName": "张经理",
"contactPhone": "13812345678",
"contactEmail": "zhang@guangzhou-xx.com",
"contactWechat": "zhangmanager",
"bankName": "招商银行广州天河支行",
"bankAccount": "6225880012345678",
"bankAccountName": "广州市XX电子科技有限公司",
"taxNo": "91440101MA5XXXXX4G",
"moq": 100,
"leadTimeDays": 7,
"monthlyCapacity": 5000,
"currency": "CNY",
"paymentDays": 30,
"remark": "重点开发的工厂,产品质量稳定"
}
② 审核拒绝 - 请求体:
PUT /api/srm/suppliers/1748291234567890/reject
Content-Type: application/json
{
"auditRemark": "营业执照已过期(有效期至2024-12-31),请更新后重新提交。质检报告缺失,需上传最新质检报告。"
}
③ 供应商下拉选项 - 响应(用于采购单选择供应商时):
GET /api/srm/suppliers/options?categoryId=3
{
"code": 200,
"data": [
{
"value": "1748291234567890",
"label": "广州市XX电子科技有限公司",
"supplierCode": "SUP-20250117-0001",
"grade": "A",
"score": 88.50,
"moq": 100,
"leadTimeDays": 7,
"contactName": "张经理",
"contactPhone": "138****5678"
}
]
}
10.3 接口错误码规范
供应商模块专用错误码范围:11000 - 11099
| 错误码 | 含义 | 场景 |
|---|---|---|
| 11001 | 供应商不存在 | 根据ID查不到数据 |
| 11002 | 供应商状态不允许此操作 | 状态流转非法 |
| 11003 | 供应商名称已存在 | 同一租户内重名 |
| 11004 | 供应商邮箱已被使用 | 邮箱重复 |
| 11005 | 该供应商有未完成的采购单,不能删除 | 删除时关联检查 |
| 11006 | 审核拒绝原因不能为空 | 拒绝时必填原因 |
| 11007 | 提交人不能审核自己的供应商 | 利益冲突防护 |
| 11008 | 必须上传营业执照才能提交审核 | 资质完整性检查 |
| 11009 | 资质文件类型不合法 | 文件校验失败 |
| 11010 | 资质文件超过大小限制(10MB) | 文件校验失败 |
第十一节 前端页面设计
11.1 供应商管理模块页面地图
graph TD
A["供应商管理模块<br>/srm/supplier"] --> B["供应商列表页<br>index.vue<br>主页面"]
B --> C["新增供应商抽屉<br>SupplierForm.vue<br>右侧滑出抽屉"]
B --> D["供应商详情页<br>detail.vue<br>点击名称跳转"]
D --> E["基本信息Tab<br>BasicInfo.vue"]
D --> F["资质文件Tab<br>CertFiles.vue<br>含上传功能"]
D --> G["联系人Tab<br>Contacts.vue"]
D --> H["评分记录Tab<br>ScoreHistory.vue<br>含折线图"]
D --> I["审核日志Tab<br>AuditLogs.vue"]
B --> J["审核操作弹窗<br>AuditDialog.vue<br>通过/拒绝"]
11.2 供应商列表页面结构
┌─────────────────────────────────────────────────────┐
│ 供应商管理 [+ 新增供应商] │
├─────────────────────────────────────────────────────┤
│ 🔍 搜索区域(折叠展开) │
│ 供应商名称 [___________] 类型 [全部 ▼] │
│ 状态 [全部 ▼] 评级 [全部 ▼] [搜索] [重置] │
├───────┬────────┬────────┬─────┬──────┬──────┬──────┤
│ 编码 │ 名称 │ 类型 │ 评级 │ 评分 │ 状态 │ 操作 │
├───────┼────────┼────────┼─────┼──────┼──────┼──────┤
│SUP-.. │广州XX. │ 工厂 🏭 │ A │ 88.5 │ ✅通过│ 查看 │
│ │ │ │ │ │ │ 编辑 │
│ │ │ │ │ │ │ 审核 │
├───────┼────────┼────────┼─────┼──────┼──────┼──────┤
│SUP-.. │深圳YY. │贸易 🏪 │ B │72.3 │⏳待审 │查看 │
│ ⚠️资质│ │ │ │ │ │撤回 │
│ 即将过│ │ │ │ │ │ │
│ 期 │ │ │ │ │ │ │
├───────┴────────┴────────┴─────┴──────┴──────┴──────┤
│ 共 156 条 每页 20 ▼ < 1 >│
└─────────────────────────────────────────────────────┘
11.3 新增/编辑供应商表单设计
表单采用分步骤(Steps)设计,降低信息填写的压力感:
步骤一:基础信息 步骤二:财务信息 步骤三:供货能力
━━━━━━━━━━━━━━━━ ───────────────── ─────────────────
供应商名称 * 开户银行 最小起订量(MOQ)
[___________________] [___________________] [_______] 件
供应商类型 * 银行账号 交货周期(Lead Time)
[○工厂 ○贸易商 ○物流] [___________________] [_______] 天
供货品类 * 开户名称 月最大产能
[品类多选框] [___________________] [_______] 件
所在地区 税号 结算货币
[省份 ▼] [城市 ▼] [___________________] [CNY ▼]
主联系人姓名 * 开票类型 账期
[___________________] [○专票 ○普票 ○收据] [0天/30天/60天 ▼]
联系电话 *
[___________________]
邮箱 *(Portal登录账号)
[___________________]
备注
[多行文本框]
[上一步] [下一步] [上一步] [保存草稿] [提交审核]
11.4 供应商详情页面结构
┌──────────────────────────────────────────────────────────────┐
│ ← 返回列表 │
│ 广州市XX电子科技有限公司 SUP-20250117-0001 │
│ [工厂供应商] [A级] [88.5分] [已通过 ✅] │
│ [编辑] [停用] [Portal: 已开通] │
├──────┬────────┬──────────┬──────────┬──────────────────────┤
│基本信│资质文件│ 联系人 │ 评分记录│ 审核日志 │
├──────┴────────┴──────────┴──────────┴──────────────────────┤
│ │
│ 基本信息 │
│ ┌─────────────┬──────────────────────────────────────┐ │
│ │ 供应商编码 │ SUP-20250117-0001 │ │
│ │ 公司类型 │ 工厂供应商 │ │
│ │ 所在地区 │ 广东省 广州市 │ │
│ │ 主联系人 │ 张经理 138****5678 查看完整号码 │ │
│ │ 联系邮箱 │ zhang@guangzhou-xx.com │ │
│ │ 创建时间 │ 2025-01-17 10:30:00 │ │
│ └─────────────┴──────────────────────────────────────┘ │
│ │
│ 供货能力 │
│ ┌─────────────┬──────────────────────────────────────┐ │
│ │ 最小起订量 │ 100 件 │ │
│ │ 交货周期 │ 7 天 │ │
│ │ 月最大产能 │ 5,000 件 │ │
│ │ 结算货币 │ CNY(人民币) │ │
│ │ 账期 │ 月结30天 │ │
│ └─────────────┴──────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
11.5 评分趋势图设计
评分历史 Tab 中展示折线图,直观呈现供应商绩效走势:
评分趋势(近12个月) 当前评级:A级
100 │
90 │ ●─────●─────●─────●
80 │ ●───● ●─────●
70 │───● ●
60 │────────────────────────────────────── 60分警戒线(虚线)
50 │
└─────────────────────────────────────
8月 9月 10月 11月 12月 1月 2月
● 综合评分趋势 --- 60分警戒线
各维度本月详情:
准时交货率: 90% → 22.5分
质量合格率: 95% → 23.75分
响应速度评分: 22分(平均4.5小时响应)
价格竞争力: 20分(略高于市场均价)
综合得分: 88.25分
11.6 前端关键交互逻辑
状态按钮的显示/隐藏控制逻辑:
flowchart TD
A["供应商列表每行<br>渲染操作按钮"] --> B{"供应商当前状态"}
B -- "草稿(0)" --> C["显示:编辑 | 提交审核 | 删除"]
B -- "待审核(1)" --> D{"当前用户角色"}
D -- "采购专员" --> E["显示:查看(只读)"]
D -- "采购负责人" --> F["显示:查看 | 审核通过 | 审核拒绝 | 要求补充"]
B -- "已通过(2)" --> G["显示:查看 | 编辑(部分字段)| 停用"]
B -- "已拒绝(3)" --> H["显示:查看 | 编辑 | 重新提交 | 删除"]
B -- "已停用(4)" --> I["显示:查看 | 重新启用"]
文件上传组件核心交互:
flowchart TD
A["用户选择文件"] --> B["前端校验<br>大小/格式/数量"]
B --> C{"校验通过?"}
C -- 否 --> D["显示错误提示<br>阻止上传"]
C -- 是 --> E["显示进度条<br>开始上传"]
E --> F["调用后端上传接口<br>/api/file/upload"]
F --> G{"上传成功?"}
G -- 成功 --> H["显示文件预览卡片<br>文件名 + 大小 + 上传时间"]
G -- 失败 --> I["显示失败原因<br>提供重试按钮"]
H --> J["用户可以在线预览<br>点击文件名打开预览"]
H --> K["用户可以删除<br>点击删除图标移除"]
今日总结与作业
今日知识点回顾
业务层面:
- 供应商的三种类型(工厂/贸易商/物流服务商)及其业务区别
- 供应商全生命周期:注册(采购员提交审核)→审核(采购负责人)→合作→评估→淘汰
- 状态机设计:5个状态、8种状态流转、每种状态的可执行操作
- 供应商分层分级:S/A/B/C四级评分体系和差异化合作策略
- 多供应商策略:同品类至少2家以上,实现柔性响应
- Portal协同体系:供应商外部登录,严格权限隔离
- 消息通知机制:站内信(WebSocket) + 邮件双通道,关键节点必触发
技术层面:
- 5张数据库表设计:主表/资质文件/联系人/评分记录/审核日志 (供应商的基础操作表)
- 供应商编码自动生成:SUP-YYYYMMDD-XXXX 格式,防并发冲突
- 文件上传安全:Magic Bytes 文件真实类型检测,OSS 路径规范 (大小 格式 路径 目录)
- 评分计算四维度:准时交货率/质量合格率/响应速度/价格竞争力,各25分
- 幂等性设计:审核操作重复执行不会产生副作用
- 接口设计规范:RESTful风格,完整的错误码体系(11000-11099)
- 定时任务:每月1日计算评分,每天检查资质到期
今日作业
作业 1:数据库执行(必做)
-
执行本课所有建表 SQL:
supplier(主表)supplier_cert(资质文件)supplier_contact(联系人)supplier_score_log(评分记录)supplier_audit_log(审核日志)sys_message(站内消息)
-
执行字典数据 SQL(供应商资质类型/联系人类型/公司规模)
-
插入测试数据并验证:
-- 插入3条测试供应商数据(不同状态)
INSERT INTO `supplier` (
`id`, `tenant_id`, `supplier_code`, `supplier_name`, `supplier_type`,
`province`, `city`, `contact_name`, `contact_phone`, `contact_email`,
`moq`, `lead_time_days`, `currency`, `payment_days`,
`grade`, `score`, `status`, `create_by`, `create_time`, `update_time`
) VALUES
-- 已通过的A级供应商
(1001, 101, 'SUP-20250117-0001', '广州市鑫源电子科技有限公司', 1,
'广东省', '广州市', '李总监', '13812345678', 'li@xinyuan.com',
200, 7, 'CNY', 30, 'A', 88.50, 2, 501, NOW(), NOW()),
-- 待审核的供应商
(1002, 101, 'SUP-20250117-0002', '深圳市博通贸易有限公司', 2,
'广东省', '深圳市', '王经理', '13987654321', 'wang@botong.com',
50, 3, 'CNY', 0, 'C', 0.00, 1, 502, NOW(), NOW()),
-- 草稿状态的供应商
(1003, 101, 'SUP-20250117-0003', '义乌市精品小商品店', 2,
'浙江省', '义乌市', '陈老板', '13611112222', 'chen@yiwu.com',
10, 5, 'CNY', 0, 'C', 0.00, 0, 501, NOW(), NOW());
-- 为已通过的供应商插入审核日志
INSERT INTO `supplier_audit_log` (
`id`, `tenant_id`, `supplier_id`, `from_status`, `to_status`,
`action`, `audit_remark`, `operator_id`, `operator_name`, `operate_time`
) VALUES
(2001, 101, 1001, 0, 1, '提交审核', NULL, 501, '张采购员', '2025-01-17 09:00:00'),
(2002, 101, 1001, 1, 2, '审核通过', '资质完整,信息真实,批准合作', 502, '李采购负责人', '2025-01-17 10:30:00');
-- 验证查询:查看各状态供应商数量
SELECT
status,
CASE status
WHEN 0 THEN '草稿'
WHEN 1 THEN '待审核'
WHEN 2 THEN '已通过'
WHEN 3 THEN '已拒绝'
WHEN 4 THEN '已停用'
END AS status_name,
COUNT(*) AS count
FROM supplier
WHERE tenant_id = 101 AND is_deleted = 0
GROUP BY status
ORDER BY status;
作业 2:接口设计练习(必做)
按照本节课的接口规范,手动设计以下接口的请求和响应:
-
查询供应商列表接口:
- 请求参数有哪些?数据类型是什么?
- 响应数据结构是什么样的?
- 如果
keyword参数为空,SQL 中该如何处理?
-
审核通过接口:
- 接口路径是什么?
- 需要做哪些业务校验(写出至少5条)?
- 供应商必须存在,且未被逻辑删除。
- 供应商必须属于当前租户,不能跨租户审核。
- 当前用户必须有 srm:supplier:audit 权限。
- 供应商当前状态必须是 待审核 status=1。
- 必填信息必须完整,比如供应商名称、联系人、联系电话、主营品类等。
- 必须已上传必要资质,比如营业执照。
- 如果已存在 Portal 账号,不能重复创建。
- 审核备注长度、格式需要校验。
- 审核通过后需要做哪些后续操作(写出完整步骤)?
- 查询供应商当前状态。
- 判断是否为待审核状态。
- 更新供应商状态为已通过 status=2。
- 初始化供应商评级,例如默认 C 级或初始分。
- 创建供应商 Portal 账号,写入 sys_user。
- 给账号分配供应商角色 ROLE_SUPPLIER。
- 回写 supplier.portal_user_id 和 portal_enabled=1。
- 写入供应商审核日志 supplier_audit_log。
- 发送站内信通知采购专员。
- 发送邮件给供应商联系人,告知 Portal 登录地址和初始账号。
- 返回审核成功结果。
项目中的每个业务 不用全部都看 只看你自己想要准备的接口/业务 也就是说 你对哪个接口感兴趣 回头要写到简历上的 那你就必须要把相关业务流程(文字版)和实现逻辑(代码版) 都必须搞懂
每个同学 都必须至少要掌握 4到5个核心的业务 最好是不要跨太多的模块/系统 一个系统中 搞3个 一个系统中搞2个
作业 3:业务分析题(必做)
请回答以下业务问题:
-
状态机安全:系统如何防止有人通过篡改请求参数,把”待审核”直接跳变为”已完成”(绕过审核)?在哪一层做校验最合适,为什么? 不能允许前端直接传一个
status=2或status=99就修改供应商状态。状态流转必须由后端控制,前端只能调用明确的业务动作接口 最合适的校验层是 Service 业务层。 原因是 Controller 只负责接收请求,数据库约束只能保证字段合法,真正的业务规则在 Service 层最清楚。Service 层需要判断“当前状态 + 当前操作 + 用户权限”是否合法。比如只有待审核状态才能审核通过,已停用供应商不能直接变成草稿。在状态机类中已经限定了 固定的状态值 固定的调整的流程
-
并发问题:两个采购负责人同时打开了同一个供应商的审核页面,几乎同时点击了”审核通过”。请描述如果没有幂等处理会发生什么?有了幂等处理后系统如何保证正确性? 如果没有幂等处理,两个采购负责人同时点击审核通过,可能出现:
- 两次更新供应商状态;
- 创建两个 Portal 账号;
- 发送两封开通邮件;
- 写入两条重复审核日志;
- 数据状态不一致,后续排查困难。
有幂等处理后,系统应该这样保证正确性:
- 审核前查询当前状态,只有
status=1才允许审核。 - 更新时带状态条件 CAS 原子性更新
- 如果影响行数是 1,说明本次审核成功。
- 如果影响行数是 0,说明已经被别人审核过,直接返回“已审核成功”,不重复创建账号。
- Portal 账号创建也要有唯一约束,防止并发重复创建。
- 在供应商表中有一个字段
portal_user_id标记当前的供应商 是否已经完成了用户的注册 - 这个字段的更新 和 对供应商用户账号的注册 要放到同一个事务中
- 在供应商表中有一个字段
-
评分数据缺失:某供应商本月没有下过任何采购单,请描述评分定时任务应该如何处理这种情况?直接给0分合理吗?为什么? 如果某供应商本月没有任何采购单,不应该直接给 0 分。 因为 0 分代表供应商表现极差,但本月没有采购数据并不等于它交货差、质量差、响应慢。直接给 0 分会导致评分失真,甚至错误地把正常供应商降为 C 级。 更合理的处理方式:
- 本月不重新计算评分。
- 沿用上月评分和评级。
- 在评分记录中写入说明:
本月无采购数据,评分沿用上月 - 如果连续多个月没有合作,可以单独标记为“长期未合作”或“待复核供应商”,而不是用低分惩罚。
作业 4:数据库设计题(选做)
根据以下新需求,补充设计数据库表:
新需求:支持供应商自己上传发货单据(装箱单/出库单/物流面单图片),这些文件与资质文件不同,是针对每一笔采购单的,需要关联到具体的采购单。
请完成:
- 设计
supplier_delivery_doc(发货单据表)的完整字段 - 说明该表与
supplier表、purchase_order(采购单)表的关联关系 - 写出建表 SQL
今日技术亮点
1. 供应商审核状态机
业务点:供应商从草稿、待审核、已通过、已拒绝、已停用之间流转,不能乱跳状态。
技术点:状态机设计 + 状态流转校验 + 审核日志
可以面试这样讲:
我负责供应商审核流程,使用状态机约束供应商生命周期。比如只有“待审核”的供应商才能审核通过,已停用供应商不能直接下采购单。每次状态变化都会写入审核日志,方便后续追溯是谁在什么时间做了什么操作。
这是很自然的亮点,因为供应商审核本来就需要状态控制。
2. 审核接口幂等设计
业务点:审核通过时要更新状态、创建 Portal 账号、发送通知。如果用户重复点击或网络重试,不能创建多个账号。
技术点:幂等校验 + 状态前置判断 + 唯一约束兜底
可以面试这样讲:
审核通过接口做了幂等处理。接口执行前会判断当前供应商是否仍是“待审核”状态,如果已经通过,就直接返回成功,不重复创建 Portal 账号。同时 portal_user_id 和用户账号也做唯一性约束,防止并发请求造成脏数据。在进行更新的时候 要使用CAS 更新 确保状态是 待审核 才能改成 审核通过
3. 供应商编码生成的并发优化
业务点:供应商编码要唯一,例如 SUP-20250117-0001。
技术点:Redis INCR 原子自增 + 数据库唯一索引兜底
可以面试这样讲:
供应商编码没有采用每次查询数据库最大值再加一的方式,因为高并发下容易重复,也会增加数据库压力。我使用 Redis INCR 按租户和日期生成自增序号,例如 supplier:code:seq:{tenantId}:{yyyyMMdd},再用数据库唯一索引兜底,保证编码唯一。
这个亮点能自然引出 Redis 原子性、并发冲突、降级方案。
4. 文件上传安全校验
业务点:供应商需要上传营业执照、认证证书、授权文件等资质。
技术点:扩展名白名单 + MIME 初筛 + Magic Bytes 校验 + OSS/MinIO 存储
可以面试这样讲:
供应商资质上传不能只看文件后缀,因为攻击者可能把脚本文件改成 jpg 或 pdf。我做了多层校验:限制文件大小、校验扩展名、读取 Magic Bytes 判断真实文件类型,然后统一重命名后上传到 OSS,路径按租户和业务类型隔离。
5. 资质文件到期提醒任务
业务点:营业执照、授权证书、质量认证都有有效期,到期后不能继续作为有效供应商资质。
技术点:XXL-JOB 定时任务 + 分片处理 + 通知去重
可以面试这样讲:
我实现了供应商资质到期提醒任务,每天扫描 30 天、7 天、1 天内即将到期的资质文件,并通知采购专员和负责人。数据量大时可以用 XXL-JOB 分片,按 tenant_id 或证书 ID 分片处理,同时通过提醒记录表避免重复发送。
分片广播 和 线程池异步处理
6. 供应商绩效评分模型
业务点:卖家不能只凭感觉选供应商,需要根据交期、质量、响应、价格综合评估。
技术点:定时评分任务 + 多维度加权模型 + 评分快照/历史记录
可以面试这样讲:
我实现了供应商绩效评分模型,从采购单、入库质检、询价响应和采购价格中提取数据,按准时交货率、质量合格率、响应速度、价格竞争力四个维度加权计算总分,并按月写入评分记录表,保留历史趋势。
重业务逻辑 能够讲清楚 评分的过程 四个维度
7. 供应商风险预警
业务点:某个品类只有一家供应商,或者供应商连续低评级,会导致断供风险。
技术点:风险规则引擎雏形 + 站内信/邮件通知 + 风险事件记录
可以面试这样讲:
我做了供应商风险预警规则,比如某品类可用供应商少于 2 家,或者所有供应商评级低于 B,就自动生成风险事件,并通过站内信和邮件提醒采购负责人开发备选供应商。这样可以提前发现断供风险,而不是等缺货后再补救。
风险表 数据 怎么进来的 什么时候标记为 已解除
8. 供应商 Portal 数据隔离
业务点:供应商登录后只能看自己的采购单和对账信息,不能看到其他供应商、库存、利润数据。
技术点:tenant_id 多租户隔离 + supplier_id 业务隔离 + RBAC 权限控制
可以面试这样讲:
Portal 端除了多租户隔离,还要做供应商维度的数据隔离。tenant_id 只能保证不同卖家之间数据隔离,但同一个卖家下面可能有多个供应商,所以 Portal 查询采购单时还必须追加 supplier_id = 当前登录供应商ID,防止供应商之间互相看到数据。
这是非常好的面试点,因为它能体现你真的理解 SaaS 和业务权限。
9. 消息通知机制
业务点:审核、资质到期、评级变化、风险预警都需要通知不同角色。
技术点:业务事件 + 站内信 + 邮件通知 + 异步发送
可以面试这样讲:
我把供应商审核、评级变化、证照到期等场景抽象成业务事件,系统根据事件类型找到接收人,生成站内信,重要事件再发送邮件。通知发送可以异步处理,避免影响主业务接口响应。
如果要在简历上写 SRM 供应商系统 可以围绕着下面这些点来写
负责 SRM 供应商管理模块,完成供应商档案、审核状态机、资质文件管理、供应商 Portal 协同和绩效评分功能。
使用状态机控制供应商审核流程,结合幂等校验和审核日志保证审核操作可追溯、可重复提交不产生脏数据。
使用 Redis INCR 生成供应商业务编码,并通过数据库唯一索引兜底,解决并发新增供应商时编码冲突问题。
实现供应商资质文件上传与到期提醒,上传过程结合文件大小、扩展名、Magic Bytes 校验和 OSS 存储,到期提醒通过 XXL-JOB 定时扫描并发送站内信/邮件。
设计供应商绩效评分模型,基于准时交货率、质量合格率、响应速度、价格竞争力按月计算供应商评分,并触发低评级风险预警。
明日预告
第三天:采购管理系统(PMS)完整实现
明天将完整实现从采购需求产生到入库付款的完整采购全链路,包括:
- 采购需求来源:库存预警触发 / 销售预测 / 手动申请 三种模式
- 询价比价模块:多供应商报价对比和自动评分
- 采购订单状态机:8个状态,完整状态流转
- 采购入库与库存联动(WMS联动核心)
- 应付账款管理和账期到期提醒
- 采购数据统计报表
预习建议:
- 回顾状态机设计概念(今天已学,明天有更复杂的采购状态机)
- 理解”主从表”的数据库设计模式(采购订单主表 + 订单明细表)
- 了解什么是”业务幂等”和”事务”(采购入库需要保证库存更新的原子性)
学习提示:今天的内容涉及大量业务流程细节,这些细节在面试中非常有价值。面试官往往会问”你们的供应商审核流程是怎么设计的?”、“如何防止状态流转被绕过?“等问题。建议把今天的状态机图和审核流程图打印出来贴在桌上,多看几遍。