运营端位于 seckill-admin, 功能围绕活动的配置与管理展开, 入口分三类: 登录、主域视图 (选品数据源)、活动管理与选品。
登录 (UserController)
入口为 POST /user/login, 仅一个登录方法: 调用 support 提供的 UserDubboService 完成凭证校验与 JWT 签发。网关侧虽有统一的鉴权/授权体系,但登录接口本身在白名单中 (未登录也要能访问), 所以 admin 侧要再做一次角色校验------只允许管理员账号登录管理后台:
java
@PostMapping ("/login")
public Result<LoginResponse> login (@Valid @RequestBody LoginRequest request) {
LoginResponse response = userService.login (request);
Integer roleCode = response.getUser () == null ? null : response.getUser (). getRole ();
if (Role.fromCode (roleCode) != Role. ADMIN) {
throw new BizException (ResultEnum.FORBIDDEN.getCode (), "仅管理员可登录管理后台");
}
return Result.success (response);
}
主域视图 (SupportProductController)
主域只以 Dubbo 对外暴露能力, 浏览器无法直接访问, 因此需要为选品在 admin 中提供一个主域商品与 SKU 的只读转发入口。它持有 support 的 ProductDubboService 与 SkuDubboService, 提供四个查询:
| 端点 | 用途 |
|---|---|
GET /support/product/list |
主域商品分页(选品列表页) |
GET /support/product/{spuNo} |
单个主域商品 |
GET /support/product/{spuNo}/skus |
商品(SPU)下全部 SKU(勾选数据源) |
GET /support/product/sku/{skuNo} |
单个主域 SKU |
活动管理与配置 (ActivityController)
其余功能全部收在 ActivityController 中 (活动管理、配置、选品统一入口)。要点: 在 seckill-base 里活动与 SKU 是严格分开的两个子域, admin 侧需要聚合两者来处理。
生命周期: 管理端只提供暂停、继续、关闭 (激活由定时任务自动完成)------直接复用 base 活动侧接口;
创建、更新、删除: 只与秒杀域活动有关, 同样直接复用 base 接口;
查询:列表 (全量/按状态) 与单个查询直接复用 base; 另多一个"详情", 由 admin 聚合"活动 + 该活动下的 SKU 列表"返回 (ActivityDetailVO), 供详情页一次拉取。
活动选品 : 跨域编排
选品流程: 前端先经 SupportProductController 展示主域商品与 SKU, 运营勾选并设置秒杀价、划拨库存等, 再提交给 ActivityController 完成添加。除库存外的字段都是静态配置,唯独库存必须保证"总量守恒" (主域库存与秒杀域配额此消彼长), 因此这一步是跨域写操作, 由 admin 做两段事务编排。
添加 (主域划拨 → 秒杀域落库)
java
// 阶段一:批量划拨 (support 域内单事务: 整批扣 / 零扣)
try {
skuService.batchDeductStock (stockItems); // 原子扣减 + 按 skuNo 排序防死锁
} catch (Exception e) {
BizException biz = findBizException (e);
if (biz != null) throw biz; // 确认失败: 已回滚, 零副作用, 直接上抛
// 未知结果: 不动作、转人工
throw new BizException (SYSTEM_ERROR, "库存划拨结果未知,请人工核对主域库存后再决定是否重试");
}
// 阶段二:秒杀域批量落库 (base 域内单事务: 整批插 / 零插)
try {
seckillProductSkuService.batchAdd (request);
} catch (Exception e) {
BizException biz = findBizException (e);
if (biz != null) {
restoreStock (activityNo, stockItems, "添加失败补偿归还"); // 确认失败 → 整批补偿归还
throw biz;
}
// 未知结果: 查证已落库则按成功返回, 否则转人工
if (! verifyAdded (activityNo, stockItems)) {
throw new BizException (SYSTEM_ERROR, "落库结果未知,请人工核对活动商品后再决定是否重试");
}
}
主域扣减的两道防护: CAS 语义的原子扣减 (WHERE stock >= quantity, 数量不足则影响 0 行)+ 批量按 skuNo 排序执行 (多活动同时划拨同一批 SKU 时, 统一加锁顺序、不会死锁)。
删除 (秒杀域删除 → 主域归还)
秒杀域删除是整体删行, 删完之后行就消失、配额不可再查------所以归还数量必须先预读快照, 它是"超时后恢复归还"的唯一依据:
java
List<StockChangeItem> preReadItems = preReadQuota (activityNo, skuNos); // 预读配额快照
try {
restoreItems = seckillProductSkuService.batchRemove (request)...; // 阶段一: 秒杀域批量删
} catch (Exception e) {
if (确认失败) throw biz; // 零删除, 无需归还
restoreItems = verifyRemoved (activityNo, skuNos, preReadItems); // 未知: 查证是否已删
if (restoreItems == null) { /* 未删/读取失败 → 转人工 */ }
}
restoreStock (activityNo, restoreItems, "删除归还"); // 阶段二: 主域归还
失败裁决原则
| 结果形态 | 判定方式 | 处理 |
|---|---|---|
| 确认失败 | 异常链含 BizException(事务已回滚=零副作用) |
补偿归还可安全重试(≤3 次);或直接上抛 |
| 未知结果(超时等) | 无 BizException |
不重试写操作;能查证的先查证(base 侧有查询接口),查无结论转人工 |
| 查证手段 | 只做读(verifyAdded / verifyRemoved) | 秒杀域可查证;主域库存无查询能力 → 归还超时只能转人工 |
涉及库存的写操作,未知状态下绝不重试写 (防止双重效果); 重试只用于"确认失败后的补偿"与"读查证"------宁可留给人工核对, 不可重复动库存。
添加与删除跨两域执行, 但没有任何"横跨两域的锁": 每个域各自"单事务 + 失败补偿", 跨域层面不存在持锁等待; 域内层面的等待 (活动行闸门、排序后的行锁) 只排队、不成环。