资产管理系统

物品管理模块

物品申领系统接口文档

业务功能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)

参数名类型是否必填描述示例对应数据库字段/逻辑
pageNumInteger页码,从1开始,默认 11LIMIT 分页计算
pageSizeInteger每页记录数,默认 1020LIMIT 分页计算
keywordString关键字模糊匹配(资产名称/资产编号)"笔记本电脑"property_details.property_name LIKE %keyword% 或 property.property_number LIKE %keyword%
categoryIdLong资产分类ID(按字典库分类筛选)5property_details.property_dictionary_id = ?
warehouseIdLong仓库ID(筛选指定仓库的资产)3property.warehouse_id = ?
brandString品牌模糊匹配"联想"property_details.property_brand LIKE %brand%
modelString型号模糊匹配"X1"property_details.property_model LIKE %model%
sortByString排序方式(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要点

  1. 核心查询逻辑
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};
  1. 分页计算
    • offset = (pageNum - 1) * pageSize
    • total 通过 COUNT(*) 查询获取(使用相同的WHERE条件)
  2. 可申领条件
    • 资产状态必须为闲置(property_state.property_state = 1
    • 资产未被删除(property.deleted = 0
    • 资产未被冻结(property.is_delete = 0,如果适用)

8. 注意事项

  • 权限控制: 仅允许已登录用户访问,可能需要校验用户是否有权申领特定仓库的资产
  • 数据一致性: 确保propertyproperty_detailsproperty_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-TypeString必须为 application/jsonapplication/json
AuthorizationString用户认证tokenBearer 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要点

  1. 数据验证逻辑
-- 检查资产是否存在且可申领
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;
  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. 核心插入逻辑(事务操作)
-- 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);
  1. 审批流程初始化逻辑
// 根据审批规则类型初始化审批步骤
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. 完整的业务逻辑流程

  1. 验证申领资产可用性
  2. 查询适用的审批规则
  3. 开启事务
  4. 插入审批主表记录
  5. 插入申领物品明细记录
  6. 初始化审批步骤(property_approve_step)
  7. 更新资产状态
  8. 插入审批轨迹记录
  9. 提交事务
  10. 发送通知给下一审批人

11. 注意事项

  • 事务完整性: 所有数据库操作必须在同一事务中
  • 审批规则匹配: 需要准确匹配资产类型、单位、仓库等条件
  • 步骤状态管理: 正确维护property_approve_step表中的审批状态
  • 并发控制: 对资产状态更新需要加锁防止冲突
  • 通知机制: 审批步骤初始化后需要通知第一级审批人

管理购物车功能

1. 功能说明

将物品添加到申领购物车中。购物车数据主要存储在Redis中,提供高性能的临时存储,支持用户在不同设备间同步购物车内容。

2. 请求类型

POST

3. 接口URL

/api/requisition/cart/items

4. 请求头(Headers)

参数名类型是否必填描述示例
Content-TypeString必须为 application/jsonapplication/json
AuthorizationString用户认证tokenBearer xxxxxx
X-User-IdString用户ID1001

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数据结构设计

  1. 购物车主键设计
    • cart:{userId} – 用户购物车的Hash结构
    • cart:expire:{userId} – 购物车过期时间控制
  2. 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" }'
  3. 过期时间控制# 设置购物车30天过期 EXPIRE cart:1001 2592000 SETEX cart:expire:1001 2592000 "1"

9. 后端逻辑与代码要点

  1. 数据验证逻辑
// 验证物品是否存在且可申领
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;
}
  1. 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);
}
  1. 完整的业务逻辑流程
    • 验证用户身份和权限
    • 验证物品是否存在且可申领
    • 检查库存是否充足
    • 将物品添加到Redis购物车
    • 更新购物车过期时间
    • 返回购物车当前状态(物品数量和总价值)

10. 注意事项

  • 并发控制: 使用Redis事务或Lua脚本确保并发操作的一致性
  • 内存管理: 监控购物车大小,防止单个用户购物车过大
  • 数据同步: 考虑购物车数据与数据库的最终一致性
  • 过期策略: 设置合理的过期时间,定期清理过期购物车
  • 性能优化: 使用Pipeline批量操作提高Redis性能

查看购物车功能

1. 功能说明

获取当前用户的申领购物车内容。从Redis中读取购物车数据,返回完整的物品列表和统计信息。

2. 请求类型

GET

3. 接口URL

/api/requisition/cart

4. 请求头(Headers)

参数名类型是否必填描述示例
AuthorizationString用户认证tokenBearer xxxxxx
X-User-IdString用户ID1001

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操作逻辑

  1. 获取购物车所有物品
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;
}
  1. 刷新过期时间
// 每次访问购物车时刷新过期时间
redisTemplate.expire(cartKey, 30, TimeUnit.DAYS);
String expireKey = "cart:expire:" + userId;
redisTemplate.opsForValue().set(expireKey, "1", 30, TimeUnit.DAYS);

9. 完整的业务逻辑流程

  1. 验证用户身份和权限
  2. 检查Redis中是否存在该用户的购物车
  3. 获取购物车所有物品数据
  4. 计算购物车统计信息(总数量、总价值、品类数)
  5. 刷新购物车过期时间
  6. 返回购物车内容和统计信息

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);
}
}