物品管理模块
物品申领系统接口文档
业务功能 | HTTP 方法 | 接口路径 | 语义说明 |
---|---|---|---|
1. 浏览可申领物品 | GET | /api/requisition/available-items | 获取当前可申领的物品列表 |
2. 获取热门申领标签 | GET | /api/requisition/hot-keywords | 获取高频申领关键词 |
3. 提交申领单 | POST | /api/requisition/orders | 创建新的物品申领单 |
4. 获取物品详情 | GET | /api/requisition/items/{itemId} | 获取指定物品的详细信息 |
5. 获取物品图片 | GET | /api/requisition/items/{itemId}/image | 获取物品的预览图片 |
6. 管理购物车 | POST | /api/requisition/cart/items | 添加物品到申领购物车 |
7. 查看购物车 | GET | /api/requisition/cart | 获取当前申领购物车内容 |
浏览可申领物品功能
1. 功能说明
提供分页查询当前可申领的物品列表。可申领物品需满足:资产状态为闲置(property_state.property_state = 1
),且资产未被删除(property.deleted = 0
)。
2. 请求类型
GET
3. 接口URL
/api/requisition/available-items
4. 请求参数(Query Parameters)
参数名 | 类型 | 是否必填 | 描述 | 示例 | 对应数据库字段/逻辑 |
---|---|---|---|---|---|
pageNum | Integer | 否 | 页码,从1开始,默认 1 | 1 | LIMIT 分页计算 |
pageSize | Integer | 否 | 每页记录数,默认 10 | 20 | LIMIT 分页计算 |
keyword | String | 否 | 关键字模糊匹配(资产名称/资产编号) | "笔记本电脑" | property_details.property_name LIKE %keyword% 或 property.property_number LIKE %keyword% |
categoryId | Long | 否 | 资产分类ID(按字典库分类筛选) | 5 | property_details.property_dictionary_id = ? |
warehouseId | Long | 否 | 仓库ID(筛选指定仓库的资产) | 3 | property.warehouse_id = ? |
brand | String | 否 | 品牌模糊匹配 | "联想" | property_details.property_brand LIKE %brand% |
model | String | 否 | 型号模糊匹配 | "X1" | property_details.property_model LIKE %model% |
sortBy | String | 否 | 排序方式(create_time_desc /property_price_asc /property_price_desc ),默认 create_time_desc | "property_price_asc" | ORDER BY property.create_time DESC 等 |
5. 响应体结构(HTTP 200)
{
"code": 200,
"msg": "success",
"data": {
"list": [
{
"itemId": 101, // property.id
"propertyNumber": "ZC202405200001", // property.property_number
"propertyName": "笔记本电脑", // property_details.property_name
"propertyCode": "030101", // property_details.property_code
"propertyBrand": "联想", // property_details.property_brand
"propertyModel": "ThinkPad X1 Carbon", // property_details.property_model
"propertyPrice": 9500.00, // property_details.property_price
"unit": "台", // property_details.unit
"warehouseId": 3, // property.warehouse_id
"warehouseName": "市局中心库", // 关联warehouse表获取
"propertyPicture": "/images/items/laptop.jpg", // property_details.property_picture
"createTime": "2023-01-01 10:00:00" // property.create_time
},
{
"itemId": 102,
"propertyNumber": "ZC202405200002",
"propertyName": "台式计算机",
"propertyCode": "030102",
"propertyBrand": "戴尔",
"propertyModel": "OptiPlex 7090",
"propertyPrice": 6500.00,
"unit": "台",
"warehouseId": 3,
"warehouseName": "市局中心库",
"propertyPicture": "/images/items/desktop.jpg",
"createTime": "2023-01-01 11:00:00"
}
],
"total": 45,
"pageNum": 1,
"pageSize": 10,
"pages": 5
}
}
6. 错误响应
- 400 Bad Request(参数错误)
{
"code": 400,
"msg": "参数错误:仓库ID无效",
"data": null
}
- 500 Internal Server Error(服务端异常)
{
"code": 500,
"msg": "服务器内部错误",
"data": null
}
7. 后端逻辑与SQL要点
- 核心查询逻辑
SELECT
p.id AS itemId,
p.property_number AS propertyNumber,
pd.property_name AS propertyName,
pd.property_code AS propertyCode,
pd.property_brand AS propertyBrand,
pd.property_model AS propertyModel,
pd.property_price AS propertyPrice,
pd.unit,
p.warehouse_id AS warehouseId,
w.warehouse_name AS warehouseName,
pd.property_picture AS propertyPicture,
p.create_time AS createTime
FROM
property p
INNER JOIN
property_details pd ON p.property_details_id = pd.id
INNER JOIN
property_state ps ON p.property_number = ps.property_number
LEFT JOIN
warehouse w ON p.warehouse_id = w.id
WHERE
p.deleted = 0 -- 未删除的资产
AND ps.property_state = 1 -- 资产状态为闲置
AND (pd.property_name LIKE CONCAT('%', #{keyword}, '%') OR p.property_number LIKE CONCAT('%', #{keyword}, '%') OR #{keyword} IS NULL)
AND (pd.property_dictionary_id = #{categoryId} OR #{categoryId} IS NULL)
AND (p.warehouse_id = #{warehouseId} OR #{warehouseId} IS NULL)
AND (pd.property_brand LIKE CONCAT('%', #{brand}, '%') OR #{brand} IS NULL)
AND (pd.property_model LIKE CONCAT('%', #{model}, '%') OR #{model} IS NULL)
ORDER BY
CASE #{sortBy}
WHEN 'property_price_asc' THEN pd.property_price
WHEN 'property_price_desc' THEN pd.property_price DESC
ELSE p.create_time DESC -- 默认按创建时间降序
END
LIMIT #{pageSize} OFFSET #{offset};
- 分页计算
offset = (pageNum - 1) * pageSize
total
通过COUNT(*)
查询获取(使用相同的WHERE条件)
- 可申领条件
- 资产状态必须为闲置(
property_state.property_state = 1
) - 资产未被删除(
property.deleted = 0
) - 资产未被冻结(
property.is_delete = 0
,如果适用)
- 资产状态必须为闲置(
8. 注意事项
- 权限控制: 仅允许已登录用户访问,可能需要校验用户是否有权申领特定仓库的资产
- 数据一致性: 确保
property
、property_details
、property_state
三张表的关联关系正确 - 性能优化: 建议对常用查询字段建立索引,如:
property(deleted, warehouse_id)
property_state(property_number, property_state)
property_details(property_dictionary_id, property_brand, property_model)
提交申领单功能文档
1. 功能说明
创建新的物品申领单并初始化审批流程。用户选择可申领的物品及数量后提交,系统会根据审批规则自动生成审批步骤,并相应更新资产状态。
2. 请求类型
POST
3. 接口URL
/api/requisition/orders
4. 请求头(Headers)
参数名 | 类型 | 是否必填 | 描述 | 示例 |
---|---|---|---|---|
Content-Type | String | 是 | 必须为 application/json | application/json |
Authorization | String | 是 | 用户认证token | Bearer xxxxxx |
5. 请求体(Request Body)
{
"applicantId": 1001,
"applicantName": "张三",
"unitId": 5,
"unitName": "刑侦支队",
"warehouseId": 3,
"warehouseName": "市局中心库",
"reason": "日常办公需要",
"items": [
{
"propertyNumber": "ZC202405200001",
"propertyCode": "030101",
"propertyName": "笔记本电脑",
"applyQuantity": 1,
"unit": "台"
}
]
}
6. 响应体结构(HTTP 200)
{
"code": 200,
"msg": "申领单提交成功",
"data": {
"approvalId": 2434,
"approvalNumber": "SQ202409130001",
"submitTime": "2025-09-13 10:30:25",
"nextApprovers": ["李四(装备科)", "王五(财务科)"]
}
}
7. 错误响应
- 400 Bad Request(参数错误)
{
"code": 400,
"msg": "参数错误:资产ZC202405200001不存在或不可申领",
"data": null
}
- 500 Internal Server Error(服务端异常)
{
"code": 500,
"msg": "服务器内部错误",
"data": null
}
8. 后端逻辑与SQL要点
- 数据验证逻辑
-- 检查资产是否存在且可申领
SELECT p.property_number, ps.property_state, pd.property_name
FROM property p
INNER JOIN property_state ps ON p.property_number = ps.property_number
INNER JOIN property_details pd ON p.property_details_id = pd.id
WHERE p.property_number = #{propertyNumber}
AND p.deleted = 0
AND ps.property_state = 1;
- 获取审批规则
-- 根据资产类型、单位等条件获取审批规则
SELECT id, approve_type, sys_user_ids, sys_role_ids
FROM property_approve_rules
WHERE (property_codes LIKE CONCAT('%,', #{propertyCode}, ',%') OR property_codes IS NULL)
AND (amp_unit_id = #{unitId} OR amp_unit_id IS NULL)
AND (warehouse_id = #{warehouseId} OR warehouse_id IS NULL)
ORDER BY id LIMIT 1;
- 核心插入逻辑(事务操作)
-- 1. 插入审批主表 property_approve
INSERT INTO property_approve (
warehouse_id, sys_user_id_apply, nickname, unit_id, unit_name,
title, reason, create_time, approve_act_state, approval_status, curr_code
) VALUES (
#{warehouseId}, #{applicantId}, #{applicantName}, #{unitId}, #{unitName},
CONCAT(#{applicantName}, '的资产申领单'), #{reason}, NOW(), 0, -1, 101
);
SET @approvalId = LAST_INSERT_ID();
SET @approvalNumber = CONCAT('SQ', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(@approvalId, 4, '0'));
UPDATE property_approve SET title = @approvalNumber WHERE id = @approvalId;
-- 2. 插入申领物品明细 property_property_check
INSERT INTO property_property_check (
property_code, property_approve_id, apply_number, approval_state, warehouse_id
) VALUES (#{propertyCode}, @approvalId, #{applyQuantity}, 0, #{warehouseId});
-- 3. 插入审批步骤 property_approve_step
INSERT INTO property_approve_step (
property_approve_id, curr_user_ids, curr_user_state, user_ids
) VALUES (
@approvalId,
#{nextApproverIds}, -- 下一审批人ID列表,如 "1002,1003"
'pending', -- 审批状态:pending-待审批
#{allApproverIds} -- 所有审批人ID列表
);
-- 4. 更新资产状态为"申领中"
UPDATE property_state SET property_state = 2
WHERE property_number = #{propertyNumber};
-- 5. 插入审批轨迹 property_approve_trail
INSERT INTO property_approve_trail (
sys_user_id, created_time, property_approve_id, state_code
) VALUES (#{applicantId}, NOW(), @approvalId, 101);
- 审批流程初始化逻辑
// 根据审批规则类型初始化审批步骤
if (rule.getApproveType() == 0) {
// 或签:任意一个审批人通过即可
step.setCurrUserIds("1002,1003"); // 第一级审批人
step.setCurrUserState("pending");
step.setUserIds("1002,1003,1004,1005"); // 所有审批人
} else if (rule.getApproveType() == 1) {
// 会签:需要所有审批人同意
step.setCurrUserIds("1002");
step.setCurrUserState("pending");
step.setUserIds("1002,1003,1004,1005");
}
9. 审批状态说明
- approve_act_state: 审批流程状态(0:进行中/1:已完成)
- approval_status: 审批结果状态(-1:审批中/0:同意/1:拒绝)
- curr_code: 当前审批状态码(101:待审批/201:审批中/301:已通过/401:已拒绝)
- curr_user_state: 当前步骤审批状态(pending:待审批/approved:已同意/rejected:已拒绝)
10. 完整的业务逻辑流程
- 验证申领资产可用性
- 查询适用的审批规则
- 开启事务
- 插入审批主表记录
- 插入申领物品明细记录
- 初始化审批步骤(property_approve_step)
- 更新资产状态
- 插入审批轨迹记录
- 提交事务
- 发送通知给下一审批人
11. 注意事项
- 事务完整性: 所有数据库操作必须在同一事务中
- 审批规则匹配: 需要准确匹配资产类型、单位、仓库等条件
- 步骤状态管理: 正确维护property_approve_step表中的审批状态
- 并发控制: 对资产状态更新需要加锁防止冲突
- 通知机制: 审批步骤初始化后需要通知第一级审批人
管理购物车功能
1. 功能说明
将物品添加到申领购物车中。购物车数据主要存储在Redis中,提供高性能的临时存储,支持用户在不同设备间同步购物车内容。
2. 请求类型
POST
3. 接口URL
/api/requisition/cart/items
4. 请求头(Headers)
参数名 | 类型 | 是否必填 | 描述 | 示例 |
---|---|---|---|---|
Content-Type | String | 是 | 必须为 application/json | application/json |
Authorization | String | 是 | 用户认证token | Bearer xxxxxx |
X-User-Id | String | 是 | 用户ID | 1001 |
5. 请求体(Request Body)
{
"itemId": "ZC202405200001",
"propertyCode": "030101",
"propertyName": "笔记本电脑",
"quantity": 1,
"unit": "台",
"warehouseId": 3,
"warehouseName": "市局中心库",
"propertyPrice": 6500.00,
"propertyBrand": "联想",
"propertyModel": "ThinkPad X1"
}
6. 响应体结构(HTTP 200)
{
"code": 200,
"msg": "物品已成功添加到购物车",
"data": {
"cartItemCount": 5,
"totalValue": 18500.00
}
}
7. 错误响应
- 400 Bad Request(参数错误)
{
"code": 400,
"msg": "参数错误:数量必须大于0",
"data": null
}
- 404 Not Found(物品不存在)
{
"code": 404,
"msg": "物品ZC202405200001不存在或不可申领",
"data": null
}
- 500 Internal Server Error(服务端异常)
{
"code": 500,
"msg": "服务器内部错误",
"data": null
}
8. Redis数据结构设计
- 购物车主键设计
cart:{userId}
– 用户购物车的Hash结构cart:expire:{userId}
– 购物车过期时间控制
- Hash字段设计
# Key: cart:1001 # Field: ZC202405200001 # Value: JSON字符串包含物品详情 HSET cart:1001 ZC202405200001 '{ "propertyCode": "030101", "propertyName": "笔记本电脑", "quantity": 1, "unit": "台", "warehouseId": 3, "warehouseName": "市局中心库", "propertyPrice": 6500.00, "propertyBrand": "联想", "propertyModel": "ThinkPad X1", "addedTime": "2025-09-13T10:30:25Z" }'
- 过期时间控制
# 设置购物车30天过期 EXPIRE cart:1001 2592000 SETEX cart:expire:1001 2592000 "1"
9. 后端逻辑与代码要点
- 数据验证逻辑
// 验证物品是否存在且可申领
public boolean validateItem(String itemId, int quantity) {
// 查询数据库验证物品状态
String sql = "SELECT p.property_number, ps.property_state, pd.stock_quantity " +
"FROM property p " +
"INNER JOIN property_state ps ON p.property_number = ps.property_number " +
"INNER JOIN property_details pd ON p.property_details_id = pd.id " +
"WHERE p.property_number = ? AND p.deleted = 0 AND ps.property_state = 1";
// 执行查询并验证库存
return stockQuantity >= quantity;
}
- Redis操作逻辑
// 添加物品到购物车
public void addToCart(String userId, CartItem item) {
String cartKey = "cart:" + userId;
String expireKey = "cart:expire:" + userId;
// 检查物品是否已在购物车
if (redisTemplate.opsForHash().hasKey(cartKey, item.getItemId())) {
// 更新数量
CartItem existingItem = getCartItem(cartKey, item.getItemId());
existingItem.setQuantity(existingItem.getQuantity() + item.getQuantity());
redisTemplate.opsForHash().put(cartKey, item.getItemId(), serializeItem(existingItem));
} else {
// 新增物品
item.setAddedTime(new Date());
redisTemplate.opsForHash().put(cartKey, item.getItemId(), serializeItem(item));
}
// 刷新过期时间
redisTemplate.expire(cartKey, 30, TimeUnit.DAYS);
redisTemplate.opsForValue().set(expireKey, "1", 30, TimeUnit.DAYS);
}
- 完整的业务逻辑流程
- 验证用户身份和权限
- 验证物品是否存在且可申领
- 检查库存是否充足
- 将物品添加到Redis购物车
- 更新购物车过期时间
- 返回购物车当前状态(物品数量和总价值)
10. 注意事项
- 并发控制: 使用Redis事务或Lua脚本确保并发操作的一致性
- 内存管理: 监控购物车大小,防止单个用户购物车过大
- 数据同步: 考虑购物车数据与数据库的最终一致性
- 过期策略: 设置合理的过期时间,定期清理过期购物车
- 性能优化: 使用Pipeline批量操作提高Redis性能
查看购物车功能
1. 功能说明
获取当前用户的申领购物车内容。从Redis中读取购物车数据,返回完整的物品列表和统计信息。
2. 请求类型
GET
3. 接口URL
/api/requisition/cart
4. 请求头(Headers)
参数名 | 类型 | 是否必填 | 描述 | 示例 |
---|---|---|---|---|
Authorization | String | 是 | 用户认证token | Bearer xxxxxx |
X-User-Id | String | 是 | 用户ID | 1001 |
5. 请求参数
无
6. 响应体结构(HTTP 200)
{
"code": 200,
"msg": "获取购物车成功",
"data": {
"items": [
{
"itemId": "ZC202405200001",
"propertyCode": "030101",
"propertyName": "笔记本电脑",
"quantity": 1,
"unit": "台",
"warehouseId": 3,
"warehouseName": "市局中心库",
"propertyPrice": 6500.00,
"propertyBrand": "联想",
"propertyModel": "ThinkPad X1",
"addedTime": "2025-09-13T10:30:25Z"
},
{
"itemId": "ZC202405200002",
"propertyCode": "030102",
"propertyName": "台式计算机",
"quantity": 2,
"unit": "台",
"warehouseId": 3,
"warehouseName": "市局中心库",
"propertyPrice": 5000.00,
"propertyBrand": "戴尔",
"propertyModel": "OptiPlex 3080",
"addedTime": "2025-09-13T11:15:30Z"
}
],
"summary": {
"totalItems": 3,
"totalValue": 16500.00,
"itemCategories": 2
}
}
}
7. 错误响应
- 404 Not Found(购物车为空)
{
"code": 404,
"msg": "购物车为空",
"data": null
}
- 500 Internal Server Error(服务端异常)
{
"code": 500,
"msg": "服务器内部错误",
"data": null
}
8. Redis操作逻辑
- 获取购物车所有物品
public CartInfo getCart(String userId) {
String cartKey = "cart:" + userId;
// 检查购物车是否存在
if (!redisTemplate.hasKey(cartKey)) {
return null;
}
// 获取所有物品
Map<Object, Object> cartMap = redisTemplate.opsForHash().entries(cartKey);
List<CartItem> items = new ArrayList<>();
double totalValue = 0;
int totalItems = 0;
for (Map.Entry<Object, Object> entry : cartMap.entrySet()) {
CartItem item = deserializeItem((String) entry.getValue());
items.add(item);
totalValue += item.getPropertyPrice() * item.getQuantity();
totalItems += item.getQuantity();
}
// 构建返回结果
CartInfo cartInfo = new CartInfo();
cartInfo.setItems(items);
CartSummary summary = new CartSummary();
summary.setTotalItems(totalItems);
summary.setTotalValue(totalValue);
summary.setItemCategories(items.size());
cartInfo.setSummary(summary);
return cartInfo;
}
- 刷新过期时间
// 每次访问购物车时刷新过期时间
redisTemplate.expire(cartKey, 30, TimeUnit.DAYS);
String expireKey = "cart:expire:" + userId;
redisTemplate.opsForValue().set(expireKey, "1", 30, TimeUnit.DAYS);
9. 完整的业务逻辑流程
- 验证用户身份和权限
- 检查Redis中是否存在该用户的购物车
- 获取购物车所有物品数据
- 计算购物车统计信息(总数量、总价值、品类数)
- 刷新购物车过期时间
- 返回购物车内容和统计信息
10. 注意事项
- 数据验证: 返回前验证购物车中物品是否仍然可申领
- 性能优化: 使用Pipeline批量获取Redis数据
- 缓存穿透: 处理空购物车情况,防止缓存穿透
- 数据格式化: 确保返回的数据格式符合前端需求
- 异常处理: 妥善处理Redis连接异常等特殊情况
11. 扩展功能考虑
- 分页支持: 对于大型购物车支持分页获取
- 排序选项: 支持按添加时间、价格等排序
- 过滤功能: 支持按仓库、品类等过滤购物车物品
- 批量操作: 支持批量删除、修改数量等操作
控制层
package org.x.property.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.micrometer.core.instrument.config.validate.ValidationException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.x.common.base.result.Result;
import org.x.property.modules.requisition.dto.AvailableItemsDTO;
import org.x.property.modules.requisition.dto.CartItemRequestDTO;
import org.x.property.modules.requisition.dto.RequisitionOrderRequestDTO;
import org.x.property.modules.requisition.service.RequisitionService;
import org.x.property.modules.requisition.vo.AvailableItemsVO;
import org.x.property.modules.requisition.vo.CartItemsResponseVO;
import org.x.property.modules.requisition.vo.CartVO;
import org.x.property.modules.requisition.vo.RequisitionOrderResponseVO;
import java.util.Map;
@RestController
@RequestMapping("/api/requisition")
public class RequisitionController {
@Autowired
private RequisitionService requisitionService;
@GetMapping("/available-items")
public Result<Page<AvailableItemsVO>> getAvailableItems(
@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "warehouseId", required = false) Long warehouseId,
@RequestParam(value = "brand", required = false) String brand,
@RequestParam(value = "model", required = false) String model,
@RequestParam(value = "sortBy", required = false, defaultValue = "create_time_desc") String sortBy){
// 构建查询参数
AvailableItemsDTO availableItemsDTO=new AvailableItemsDTO();
availableItemsDTO.setPageNum(pageNum);
availableItemsDTO.setPageSize(pageSize);
availableItemsDTO.setKeyword(keyword);
availableItemsDTO.setCategoryId(categoryId);
availableItemsDTO.setWarehouseId(warehouseId);
availableItemsDTO.setBrand(brand);
availableItemsDTO.setModel(model);
availableItemsDTO.setSortBy(sortBy);
//获取当前登录用户ID(权限控制) - 使用Sa-Token框架
Long currentUserId = StpUtil.getLoginIdAsLong();
// 调用服务层方法查询可申请物品
Page<AvailableItemsVO> reult = requisitionService.getAvailableItems(availableItemsDTO, currentUserId);
// 返回查询结果
return Result.success(reult);
}
// 提交申请订单
@PostMapping("/orders")//提交资产申请单
public ResponseEntity<?> submitRequisitionOrder(@RequestBody RequisitionOrderRequestDTO request) {//提交资产申请单
try {
RequisitionOrderResponseVO response = requisitionService.submitOrder(request);
return ResponseEntity.ok(response);
} catch (ValidationException e) {
return ResponseEntity.badRequest().body(Map.of("code", 400, "message", e.getMessage()));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of("code", 400, "message", e.getMessage()));
}
}
@PostMapping("/cart/items")
public ResponseEntity<?> addCartItem(@RequestBody @Valid CartItemRequestDTO request,
HttpServletRequest httpRequest) {
try {
// 从请求头获取用户信息
String userId = httpRequest.getHeader("X-User-Id");
// 添加到购物车
CartItemsResponseVO response = requisitionService.addCartItem(userId, request);
return ResponseEntity.ok(Result.success(response));
} catch (ValidationException e) {
return ResponseEntity.badRequest()
.body(Result.error(400, "参数错误:" + e.getMessage()));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.error(500, "服务器内部错误"));
}
}
// 获取用户购物车内容
@GetMapping("/cart")
public ResponseEntity<?> getCart(HttpServletRequest httpRequest) {
try {
String userId = httpRequest.getHeader("X-User-Id");
CartVO.CartInfoVO cartInfo = requisitionService.getCart(userId);
return ResponseEntity.ok(Result.success(cartInfo));
} catch (Exception e) {
if ("购物车为空".equals(e.getMessage())) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Result.error(404, "购物车为空"));
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.error(500, "服务器内部错误"));
}
}
}
}
实体类层
可浏览申请物品模块实体类
package org.x.property.modules.requisition.dto;
import lombok.Data;
@Data
public class AvailableItemsDTO {//资产列表DTO,请求体
private Integer pageNum = 1;
private Integer pageSize = 10;
private String keyword;
private Long categoryId;
private Long warehouseId;
private String brand;
private String model;
private String sortBy = "create_time_desc";
}
package org.x.property.modules.requisition.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class AvailableItemsVO {//资产列表VO,响应体
private Long itemId;
private String propertyNumber;
private String propertyName;
private String propertyCode;
private String propertyBrand;
private String propertyModel;
private BigDecimal propertyPrice;
private String unit;
private Long warehouseId;
private String warehouseName;
private String propertyPicture;
private Date createTime;
}
提交申领单功能实体类
package org.x.property.modules.requisition.dto;
import lombok.Data;
import java.util.List;
@Data
public class RequisitionOrderRequestDTO {//资产申请单DTO,请求体
private Long applicantId;
private String applicantName;
private Integer unitId;
private String unitName;
private Integer warehouseId;
private String warehouseName;
private String reason;
List<RequisitionOrderRequestItemDTO> items;
@Data
public static class RequisitionOrderRequestItemDTO {
private String propertyNumber;
private String propertyCode;
private String propertyName;
private Integer applyQuantity;
private String unit;
}
}
package org.x.property.modules.requisition.vo;
import lombok.Data;
import java.util.List;
@Data
public class RequisitionOrderResponseVO {
/*{
"code": 200,
"msg": "申领单提交成功",
"data": {
"approvalId": 2434,
"approvalNumber": "SQ202409130001",
"submitTime": "2025-09-13 10:30:25",
"nextApprovers": ["李四(装备科)", "王五(财务科)"]
}
}*/
private Integer code;
private String msg;
private ApprovalData data;
@Data
public static class ApprovalData {
private Long approvalId;
private String approvalNumber;
private String submitTime;
private List<String> nextApprovers;
}
@Data
public static class ErrorResponse {
private Integer code;
private String msg;
public ErrorResponse() {}
public ErrorResponse(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
}
管理购物车功能实体类
package org.x.property.modules.requisition.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class CartItemRequestDTO {
private String itemId;
private String propertyCode;
private String propertyName;
private Integer quantity;
private String unit;
private Long warehouseId;
private String warehouseName;
private BigDecimal propertyPrice;
private String propertyBrand;
private String propertyModel;
}
package org.x.property.modules.requisition.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class CartItemsResponseVO {
private Integer cartItemCount;
private BigDecimal totalValue;
}
查看购物车功能实体类
// CartVO.java
package org.x.property.modules.requisition.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 购物车信息VO类
*/
@Data
public class CartVO {
/**
* 购物车物品项
*/
@Data
public static class CartItemVO {
private String itemId;
private String propertyCode;
private String propertyName;
private Integer quantity;
private String unit;
private Long warehouseId;
private String warehouseName;
private BigDecimal propertyPrice;
private String propertyBrand;
private String propertyModel;
private Date addedTime;
}
/**
* 购物车汇总信息
*/
@Data
public static class CartSummaryVO {
private Integer totalItems;
private BigDecimal totalValue;
private Integer itemCategories;
}
/**
* 购物车完整信息
*/
@Data
public static class CartInfoVO {
private List<CartItemVO> items;
private CartSummaryVO summary;
}
}
package org.x.property.modules.requisition.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class CartItem {//创建购物车在 Redis 中存储的实体类
private String itemId;
private String propertyCode;
private String propertyName;
private Integer quantity;
private String unit;
private Long warehouseId;
private String warehouseName;
private BigDecimal propertyPrice;
private String propertyBrand;
private String propertyModel;
private Date addedTime;
}
Mapper接口
package org.x.property.modules.requisition.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.x.property.entity.PropertyApproveRules;
import org.x.property.modules.requisition.dto.AvailableItemsDTO;
import org.x.property.modules.requisition.dto.RequisitionOrderRequestDTO;
import org.x.property.modules.requisition.vo.AvailableItemsVO;
import java.util.List;
/**
* 物品申领模块Mapper接口
* 不继承BaseMapper,只包含必要的查询方法
*/
public interface RequisitionMapper {
/**
* 查询可申领物品列表
* @param page 分页对象
* @param availableItemsDTO 查询参数
* @param currentUserId 当前用户ID
* @return 可申领物品列表
*/
List<AvailableItemsVO> selectAvailableItems(
Page<AvailableItemsVO> page,
@Param("availableItemsDTO") AvailableItemsDTO availableItemsDTO,
@Param("currentUserId") Long currentUserId);
/**
* 检查资产状态
*/
Integer getPropertyStateByNumber(@Param("propertyNumber") String propertyNumber);
/**
* 获取审批规则
*/
PropertyApproveRules getPropertyApproveRules(@Param("propertyCode") String propertyCode,
@Param("unitId") Integer unitId,
@Param("warehouseId") Integer warehouseId);
/**
* 插入审批主表记录
*/
Long insertApprove(RequisitionOrderRequestDTO request);
/**
* 更新审批编号
*/
void updateApproveNumber(@Param("approvalId") Long approvalId, @Param("approvalNumber") String approvalNumber);
/**
* 插入申领物品明细记录
*/
void insertPropertyCheck(@Param("approvalId") Long approvalId,
@Param("item") RequisitionOrderRequestDTO.RequisitionOrderRequestItemDTO item,
@Param("warehouseId") Integer warehouseId);
/**
* 更新资产状态为"申领中"
*/
void updatePropertyStateToPending(@Param("propertyNumber") String propertyNumber);
/**
* 插入审批轨迹记录
*/
void insertApprovalTrail(@Param("userId") Long userId, @Param("approvalId") Long approvalId);
/**
* 插入审批步骤
*/
void insertApproveStep(@Param("approvalId") Long approvalId, @Param("rule") PropertyApproveRules rule);
/**
* 查询数据库验证物品状态
*/
int validateItem(@Param("itemId") String itemId);
}
service层
service层接口
package org.x.property.modules.requisition.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.x.property.modules.requisition.dto.AvailableItemsDTO;
import org.x.property.modules.requisition.dto.CartItemRequestDTO;
import org.x.property.modules.requisition.dto.RequisitionOrderRequestDTO;
import org.x.property.modules.requisition.vo.AvailableItemsVO;
import org.x.property.modules.requisition.vo.CartItemsResponseVO;
import org.x.property.modules.requisition.vo.CartVO;
import org.x.property.modules.requisition.vo.RequisitionOrderResponseVO;
public interface RequisitionService {
/**
* 查询可申请物品
* @param availableItemsDTO 查询参数
* @param currentUserId 当前登录用户ID
* @return 可申请物品分页列表
*/
Page<AvailableItemsVO> getAvailableItems(AvailableItemsDTO availableItemsDTO, Long currentUserId);
/**
* 提交申领单
* @param request 申领单请求数据
* @return 申领单响应结果
*/
RequisitionOrderResponseVO submitOrder(RequisitionOrderRequestDTO request);
/**
* 添加物品到申领购物车
* @param userId 用户ID
* @param request 购物车项目请求
* @return 购物车统计信息
*/
CartItemsResponseVO addCartItem(String userId, CartItemRequestDTO request) throws Exception;
/**
* 获取用户购物车内容
* @param userId 用户ID
* @return 购物车信息
*/
CartVO.CartInfoVO getCart(String userId) throws Exception;
}
service层实体类
package org.x.property.modules.requisition.service.Impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;
import org.x.property.entity.PropertyApproveRules;
import org.x.property.modules.requisition.dto.AvailableItemsDTO;
import org.x.property.modules.requisition.dto.CartItem;
import org.x.property.modules.requisition.dto.CartItemRequestDTO;
import org.x.property.modules.requisition.dto.RequisitionOrderRequestDTO;
import org.x.property.modules.requisition.mapper.RequisitionMapper;
import org.x.property.modules.requisition.service.RequisitionService;
import org.x.property.modules.requisition.vo.AvailableItemsVO;
import org.x.property.modules.requisition.vo.CartItemsResponseVO;
import org.x.property.modules.requisition.vo.CartVO;
import org.x.property.modules.requisition.vo.RequisitionOrderResponseVO;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Service
public class RequisitionServiceImpl implements RequisitionService {
@Autowired
private RequisitionMapper requisitionMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Page<AvailableItemsVO> getAvailableItems(AvailableItemsDTO availableItemsDTO, Long currentUserId) {
// 创建分页对象
Page<AvailableItemsVO> page = new Page<>(availableItemsDTO.getPageNum(), availableItemsDTO.getPageSize());
// 调用Mapper方法查询可申请物品
requisitionMapper.selectAvailableItems(page, availableItemsDTO, currentUserId);
// 返回分页结果
return page;
}
@Override
public RequisitionOrderResponseVO submitOrder(RequisitionOrderRequestDTO request) {
// 验证申领资产可用性
for (RequisitionOrderRequestDTO.RequisitionOrderRequestItemDTO item : request.getItems()) {
// 检查资产是否存在且可申领
Integer propertyState = requisitionMapper.getPropertyStateByNumber(item.getPropertyNumber());
if (propertyState == null || propertyState != 1) {
throw new RuntimeException("参数错误:资产" + item.getPropertyNumber() + "不存在或不可申领");
}
}
// 获取审批规则
String propertyCode = request.getItems().get(0).getPropertyCode();
Integer unitId = request.getUnitId();
Integer warehouseId = request.getWarehouseId();
PropertyApproveRules rule = requisitionMapper.getPropertyApproveRules(propertyCode, unitId, warehouseId);
if (rule == null) {
throw new RuntimeException("未找到适用的审批规则");
}
// 插入审批主表记录
Long approvalId = requisitionMapper.insertApprove(request);
// 生成审批编号
String approvalNumber = "SQ" + new SimpleDateFormat("yyyyMMdd").format(new Date()) +
String.format("%04d", approvalId);
requisitionMapper.updateApproveNumber(approvalId, approvalNumber);
// 处理每个申领物品
for (RequisitionOrderRequestDTO.RequisitionOrderRequestItemDTO item : request.getItems()) {
// 插入申领物品明细记录
requisitionMapper.insertPropertyCheck(approvalId, item, request.getWarehouseId());
// 更新资产状态为"申领中"
requisitionMapper.updatePropertyStateToPending(item.getPropertyNumber());
// 插入审批轨迹记录
requisitionMapper.insertApprovalTrail(request.getApplicantId(), approvalId);
}
// 初始化审批步骤
requisitionMapper.insertApproveStep(approvalId, rule);
// 构建响应数据
List<String> nextApprovers = getNextApprovers(rule);
return buildSuccessResponse(approvalId, approvalNumber, nextApprovers);
}
private List<String> getNextApprovers(PropertyApproveRules rule) {
// 实现获取下一审批人名称列表逻辑
// 这里需要根据实际的审批规则和用户信息来获取审批人
return Arrays.asList("李四(装备科)", "王五(财务科)");
}
private RequisitionOrderResponseVO buildSuccessResponse(Long approvalId, String approvalNumber, List<String> nextApprovers) {
RequisitionOrderResponseVO response = new RequisitionOrderResponseVO();
response.setCode(200);
response.setMsg("申领单提交成功");
RequisitionOrderResponseVO.ApprovalData data = new RequisitionOrderResponseVO.ApprovalData();
data.setApprovalId(approvalId);
data.setApprovalNumber(approvalNumber);
data.setSubmitTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
data.setNextApprovers(nextApprovers);
response.setData(data);
return response;
}
/**
* 添加物品到申领购物车
* @param userId 用户ID
* @param request 购物车项目请求
* @return 购物车统计信息
*/
@Override
public CartItemsResponseVO addCartItem(String userId, CartItemRequestDTO request) throws Exception {
// 1. 验证物品是否存在且可申领(这里简化处理,实际应调用验证逻辑)
if (request.getQuantity() <= 0) {
throw new IllegalArgumentException("数量必须大于0");
}
// 2. 构造购物车项目
CartItem cartItem = convertToCartItem(request);
// 3. 添加到Redis购物车
addToCart(userId, cartItem);
// 4. 获取购物车统计信息
return getCartStatistics(userId);
}
private CartItem convertToCartItem(CartItemRequestDTO request) {
CartItem item = new CartItem();
item.setItemId(request.getItemId());
item.setPropertyCode(request.getPropertyCode());
item.setPropertyName(request.getPropertyName());
item.setQuantity(request.getQuantity());
item.setUnit(request.getUnit());
item.setWarehouseId(request.getWarehouseId());
item.setWarehouseName(request.getWarehouseName());
item.setPropertyPrice(request.getPropertyPrice());
item.setPropertyBrand(request.getPropertyBrand());
item.setPropertyModel(request.getPropertyModel());
item.setAddedTime(new Date());
return item;
}
private void addToCart(String userId, CartItem item) {
String cartKey = "cart:" + userId;
String expireKey = "cart:expire:" + userId;
// 使用Redis事务确保原子性
redisTemplate.execute(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
operations.multi();
// 检查物品是否已在购物车
Boolean hasKey = operations.opsForHash().hasKey(cartKey, item.getItemId());
if (Boolean.TRUE.equals(hasKey)) {
// 更新数量
String existingItemJson = (String) operations.opsForHash().get(cartKey, item.getItemId());
CartItem existingItem = deserializeItem(existingItemJson);
existingItem.setQuantity(existingItem.getQuantity() + item.getQuantity());
operations.opsForHash().put(cartKey, item.getItemId(), serializeItem(existingItem));
} else {
// 新增物品
operations.opsForHash().put(cartKey, item.getItemId(), serializeItem(item));
}
// 刷新过期时间
operations.expire(cartKey, 30, TimeUnit.DAYS);
operations.opsForValue().set(expireKey, "1", 30, TimeUnit.DAYS);
return operations.exec();
}
});
}
private CartItemsResponseVO getCartStatistics(String userId) {
String cartKey = "cart:" + userId;
Map<Object, Object> items = redisTemplate.opsForHash().entries(cartKey);
int itemCount = items.size();
BigDecimal totalValue = BigDecimal.ZERO;
for (Object value : items.values()) {
CartItem item = deserializeItem((String) value);
BigDecimal itemTotal = item.getPropertyPrice().multiply(new BigDecimal(item.getQuantity()));
totalValue = totalValue.add(itemTotal);
}
CartItemsResponseVO response = new CartItemsResponseVO();
response.setCartItemCount(itemCount);
response.setTotalValue(totalValue);
return response;
}
private String serializeItem(CartItem item) {
// 序列化为JSON字符串
return JSON.toJSONString(item);
}
private CartItem deserializeItem(String json) {
// 反序列化JSON字符串
return JSON.parseObject(json, CartItem.class);
}
/**
* 获取用户购物车内容
* @param userId 用户ID
* @return 购物车信息
*/
@Override
public CartVO.CartInfoVO getCart(String userId) throws Exception {
String cartKey = "cart:" + userId;
// 检查购物车是否存在
if (!redisTemplate.hasKey(cartKey)) {
throw new Exception("购物车为空");
}
// 获取所有物品
Map<Object, Object> cartMap = redisTemplate.opsForHash().entries(cartKey);
if (cartMap.isEmpty()) {
throw new Exception("购物车为空");
}
List<CartVO.CartItemVO> items = new ArrayList<>();
BigDecimal totalValue = BigDecimal.ZERO;
int totalItems = 0;
for (Map.Entry<Object, Object> entry : cartMap.entrySet()) {
CartItem item = deserializeItem((String) entry.getValue());
CartVO.CartItemVO itemVO = convertToCartItemVO(item);
items.add(itemVO);
BigDecimal itemTotal = item.getPropertyPrice().multiply(new BigDecimal(item.getQuantity()));
totalValue = totalValue.add(itemTotal);
totalItems += item.getQuantity();
}
// 构建返回结果
CartVO.CartInfoVO cartInfo = new CartVO.CartInfoVO();
cartInfo.setItems(items);
CartVO.CartSummaryVO summary = new CartVO.CartSummaryVO();
summary.setTotalItems(totalItems);
summary.setTotalValue(totalValue);
summary.setItemCategories(items.size());
cartInfo.setSummary(summary);
// 刷新过期时间
refreshCartExpiration(userId, cartKey);
return cartInfo;
}
private CartVO.CartItemVO convertToCartItemVO(CartItem item) {
CartVO.CartItemVO itemVO = new CartVO.CartItemVO();
itemVO.setItemId(item.getItemId());
itemVO.setPropertyCode(item.getPropertyCode());
itemVO.setPropertyName(item.getPropertyName());
itemVO.setQuantity(item.getQuantity());
itemVO.setUnit(item.getUnit());
itemVO.setWarehouseId(item.getWarehouseId());
itemVO.setWarehouseName(item.getWarehouseName());
itemVO.setPropertyPrice(item.getPropertyPrice());
itemVO.setPropertyBrand(item.getPropertyBrand());
itemVO.setPropertyModel(item.getPropertyModel());
itemVO.setAddedTime(item.getAddedTime());
return itemVO;
}
private void refreshCartExpiration(String userId, String cartKey) {
// 每次访问购物车时刷新过期时间
redisTemplate.expire(cartKey, 30, TimeUnit.DAYS);
String expireKey = "cart:expire:" + userId;
redisTemplate.opsForValue().set(expireKey, "1", 30, TimeUnit.DAYS);
}
}