31 KiB
Stock Master Detail And API Log Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Split inventory into master/detail storage, fix completed inbound job child-detail lookup, and persist inbound/outbound external API logs in bas_api_log with 30-day retention.
Architecture: Use a compatibility migration for inventory: keep bas_stock as the master/current compatibility table during this phase, add bas_stock_detail for material lines, and update new backend/frontend screens to read master/detail. Stored procedure stock posting writes both master and detail so existing outbound, stocktake, and dashboard readers do not break while the UI moves to the new structure. API logs use an @ApiLog annotation for inbound external callbacks and the existing BaseExternalClient choke point for outbound calls, both writing to the same table.
Tech Stack: SQL Server, Spring Boot, MyBatis XML, RuoYi controllers/services, Vue2 + Element UI, UniApp PDA where relevant.
File Structure
Database scripts
- Create:
sql/bas_stock_master_detail.sql— createsbas_stock_detail, backfills existing stock material lines, adds indexes. - Create:
sql/proc_complete_in_job_stock_detail.sql— updatesproc_complete_in_jobstock insertion section so inbound completion writesbas_stockandbas_stock_detail. - Create:
sql/bas_api_log.sql— createsbas_api_log, indexes, and cleanup procedureproc_clean_bas_api_log.
Stock backend
- Create:
ruoyi-admin/src/main/java/com/ruoyi/bas/stockdetail/domain/BasStockDetail.java— material-line inventory detail entity. - Create:
ruoyi-admin/src/main/java/com/ruoyi/bas/stockdetail/mapper/BasStockDetailMapper.java— detail mapper contract. - Create:
ruoyi-admin/src/main/java/com/ruoyi/bas/stockdetail/service/IBasStockDetailService.java— detail service contract. - Create:
ruoyi-admin/src/main/java/com/ruoyi/bas/stockdetail/service/impl/BasStockDetailServiceImpl.java— detail service implementation. - Create:
ruoyi-admin/src/main/java/com/ruoyi/bas/stockdetail/controller/BasStockDetailController.java—/bas/stockdetail/listendpoint for detail rows under a masterstockId. - Create:
ruoyi-admin/src/main/resources/mapper/bas/BasStockDetailMapper.xml— detail SQL mapping. - Modify:
ruoyi-admin/src/main/java/com/ruoyi/bas/stock/domain/BasStock.java— addList<BasStockDetail> detailsfor master/detail responses while keeping legacy query/export fields for compatibility. - Modify:
ruoyi-admin/src/main/resources/mapper/bas/BasStockMapper.xml— make main list return one master stock row perstock_id; stop using product fields as master columns in the main list. - Modify:
ruoyi-admin/src/main/java/com/ruoyi/bas/stock/service/impl/BasStockServiceImpl.java— insert/update/delete master and detail consistently for manual stock edits.
Stock frontend
- Create:
ruoyi-ui/src/api/bas/stockDetail.js— stock detail API calls. - Modify:
ruoyi-ui/src/views/bas/stock/index.vue— show master inventory in the top table and a material-detail table below the selected master row.
Completed inbound job detail fix
- Modify:
ruoyi-admin/src/main/resources/mapper/in/MydoJobDetailMapper.xml— makeselectMydoJobDetailFinishListfilter byjob_id, mirroring the active detail query. - Modify:
ruoyi-ui/src/views/in/job/index.vue— pass selected row status to detail query instead of relying on the global query status.
API logging
- Create:
ruoyi-common/src/main/java/com/ruoyi/common/annotation/ApiLog.java—@Log-style annotation. - Modify:
ruoyi-admin/src/main/java/com/ruoyi/api/domain/ApiLog.java— add direction, request method, source IP, trace ID fields. - Create:
ruoyi-admin/src/main/java/com/ruoyi/api/mapper/ApiLogMapper.java— persistence contract. - Create:
ruoyi-admin/src/main/resources/mapper/api/ApiLogMapper.xml— insert and retention SQL. - Modify:
ruoyi-admin/src/main/java/com/ruoyi/api/service/ApiLogService.java— persist to mapper instead of only printing local logs. - Create:
ruoyi-admin/src/main/java/com/ruoyi/api/aspect/ApiLogAspect.java— annotation aspect for inbound external callbacks. - Modify:
ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java— annotate WCS/ERP/MES external endpoints that are called by outside systems. - Modify:
ruoyi-admin/src/main/java/com/ruoyi/api/client/BaseExternalClient.java— ensure outbound logs includedirection=OUTBOUNDand write tobas_api_log. - Create:
ruoyi-admin/src/main/java/com/ruoyi/api/task/ApiLogCleanupTask.java— daily retention cleanup for logs older than 30 days.
Task 1: Create Stock Detail Table And Backfill Data
Files:
-
Create:
sql/bas_stock_master_detail.sql -
Step 1: Add migration SQL
Create sql/bas_stock_master_detail.sql with this content:
IF OBJECT_ID(N'dbo.bas_stock_detail', N'U') IS NULL
BEGIN
CREATE TABLE dbo.bas_stock_detail (
stock_detail_id varchar(80) NOT NULL,
stock_id nvarchar(32) NOT NULL,
product_status nvarchar(2) NULL,
lot_no nvarchar(80) NULL,
product_id nvarchar(80) NULL,
product_name nvarchar(255) NULL,
quantity decimal(18, 0) NULL,
out_quantity decimal(18, 0) NULL,
job_detail_id nvarchar(255) NULL,
stockin_detail_id nvarchar(80) NULL,
create_time datetime2(7) NULL CONSTRAINT DF_bas_stock_detail_create_time DEFAULT GETDATE(),
update_time datetime2(7) NULL,
CONSTRAINT PK_bas_stock_detail PRIMARY KEY CLUSTERED (stock_detail_id)
);
END
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.bas_stock_detail') AND name = N'IX_bas_stock_detail_stock_id')
CREATE INDEX IX_bas_stock_detail_stock_id ON dbo.bas_stock_detail(stock_id);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.bas_stock_detail') AND name = N'IX_bas_stock_detail_product')
CREATE INDEX IX_bas_stock_detail_product ON dbo.bas_stock_detail(product_id, lot_no) INCLUDE(stock_id, quantity, out_quantity);
GO
INSERT INTO dbo.bas_stock_detail (
stock_detail_id, stock_id, product_status, lot_no, product_id, product_name,
quantity, out_quantity, job_detail_id, stockin_detail_id, create_time
)
SELECT
CAST(bs.stock_id AS varchar(80)) + '-1',
bs.stock_id,
bs.product_status,
bs.lot_no,
bs.product_id,
bs.product_name,
bs.quantity,
bs.out_quantity,
bs.job_detail_id,
bs.stockin_detail_id,
ISNULL(bs.in_date, GETDATE())
FROM dbo.bas_stock bs
WHERE NOT EXISTS (
SELECT 1 FROM dbo.bas_stock_detail d WHERE d.stock_id = bs.stock_id
)
AND (bs.product_id IS NOT NULL OR bs.product_name IS NOT NULL OR bs.quantity IS NOT NULL);
GO
- Step 2: Apply migration to
FTC25009-WMS
Run:
powershell -ExecutionPolicy Bypass -File ".\scripts\apply-sql-file.ps1" -SqlFile ".\sql\bas_stock_master_detail.sql"
Expected output:
Applied SQL file to FTC25009-WMS
- Step 3: Verify backfill
Run through MCP read_data:
SELECT TOP 10 stock_id, COUNT(*) AS detail_count
FROM dbo.bas_stock_detail
GROUP BY stock_id
ORDER BY stock_id;
Expected: existing bas_stock.stock_id values have one bas_stock_detail row each.
Task 2: Add Stock Detail Backend CRUD Read Path
Files:
-
Create:
ruoyi-admin/src/main/java/com/ruoyi/bas/stockdetail/domain/BasStockDetail.java -
Create:
ruoyi-admin/src/main/java/com/ruoyi/bas/stockdetail/mapper/BasStockDetailMapper.java -
Create:
ruoyi-admin/src/main/java/com/ruoyi/bas/stockdetail/service/IBasStockDetailService.java -
Create:
ruoyi-admin/src/main/java/com/ruoyi/bas/stockdetail/service/impl/BasStockDetailServiceImpl.java -
Create:
ruoyi-admin/src/main/java/com/ruoyi/bas/stockdetail/controller/BasStockDetailController.java -
Create:
ruoyi-admin/src/main/resources/mapper/bas/BasStockDetailMapper.xml -
Step 1: Create
BasStockDetaildomain
package com.ruoyi.bas.stockdetail.domain;
import java.math.BigDecimal;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
public class BasStockDetail extends BaseEntity {
private static final long serialVersionUID = 1L;
private String stockDetailId;
private String stockId;
@Excel(name = "物料状态")
private String productStatus;
@Excel(name = "批次号")
private String lotNo;
@Excel(name = "物料编码")
private String productId;
@Excel(name = "物料名称")
private String productName;
@Excel(name = "库存数量")
private BigDecimal quantity;
@Excel(name = "出库数量")
private BigDecimal outQuantity;
@Excel(name = "子作业id")
private String jobDetailId;
@Excel(name = "入库子单id")
private String stockinDetailId;
public String getStockDetailId() { return stockDetailId; }
public void setStockDetailId(String stockDetailId) { this.stockDetailId = stockDetailId; }
public String getStockId() { return stockId; }
public void setStockId(String stockId) { this.stockId = stockId; }
public String getProductStatus() { return productStatus; }
public void setProductStatus(String productStatus) { this.productStatus = productStatus; }
public String getLotNo() { return lotNo; }
public void setLotNo(String lotNo) { this.lotNo = lotNo; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }
public BigDecimal getQuantity() { return quantity; }
public void setQuantity(BigDecimal quantity) { this.quantity = quantity; }
public BigDecimal getOutQuantity() { return outQuantity; }
public void setOutQuantity(BigDecimal outQuantity) { this.outQuantity = outQuantity; }
public String getJobDetailId() { return jobDetailId; }
public void setJobDetailId(String jobDetailId) { this.jobDetailId = jobDetailId; }
public String getStockinDetailId() { return stockinDetailId; }
public void setStockinDetailId(String stockinDetailId) { this.stockinDetailId = stockinDetailId; }
}
- Step 2: Create mapper interface
package com.ruoyi.bas.stockdetail.mapper;
import java.util.List;
import com.ruoyi.bas.stockdetail.domain.BasStockDetail;
public interface BasStockDetailMapper {
List<BasStockDetail> selectBasStockDetailList(BasStockDetail basStockDetail);
BasStockDetail selectBasStockDetailByStockDetailId(String stockDetailId);
int insertBasStockDetail(BasStockDetail basStockDetail);
int updateBasStockDetail(BasStockDetail basStockDetail);
int deleteBasStockDetailByStockDetailId(String stockDetailId);
int deleteBasStockDetailByStockIds(String[] stockIds);
}
- Step 3: Create mapper XML
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.bas.stockdetail.mapper.BasStockDetailMapper">
<resultMap type="BasStockDetail" id="BasStockDetailResult">
<result property="stockDetailId" column="stock_detail_id" />
<result property="stockId" column="stock_id" />
<result property="productStatus" column="product_status" />
<result property="lotNo" column="lot_no" />
<result property="productId" column="product_id" />
<result property="productName" column="product_name" />
<result property="quantity" column="quantity" />
<result property="outQuantity" column="out_quantity" />
<result property="jobDetailId" column="job_detail_id" />
<result property="stockinDetailId" column="stockin_detail_id" />
</resultMap>
<select id="selectBasStockDetailList" parameterType="BasStockDetail" resultMap="BasStockDetailResult">
select stock_detail_id, stock_id, product_status, lot_no, product_id, product_name,
quantity, out_quantity, job_detail_id, stockin_detail_id
from bas_stock_detail
<where>
<if test="stockId != null and stockId != ''">and stock_id = #{stockId}</if>
<if test="productId != null and productId != ''">and product_id = #{productId}</if>
<if test="productName != null and productName != ''">and product_name like concat('%', #{productName}, '%')</if>
<if test="lotNo != null and lotNo != ''">and lot_no = #{lotNo}</if>
</where>
order by stock_detail_id
</select>
<select id="selectBasStockDetailByStockDetailId" parameterType="String" resultMap="BasStockDetailResult">
select stock_detail_id, stock_id, product_status, lot_no, product_id, product_name,
quantity, out_quantity, job_detail_id, stockin_detail_id
from bas_stock_detail
where stock_detail_id = #{stockDetailId}
</select>
<insert id="insertBasStockDetail" parameterType="BasStockDetail">
insert into bas_stock_detail(stock_detail_id, stock_id, product_status, lot_no, product_id, product_name, quantity, out_quantity, job_detail_id, stockin_detail_id)
values(#{stockDetailId}, #{stockId}, #{productStatus}, #{lotNo}, #{productId}, #{productName}, #{quantity}, #{outQuantity}, #{jobDetailId}, #{stockinDetailId})
</insert>
<update id="updateBasStockDetail" parameterType="BasStockDetail">
update bas_stock_detail
<trim prefix="set" suffixOverrides=",">
<if test="productStatus != null">product_status = #{productStatus},</if>
<if test="lotNo != null">lot_no = #{lotNo},</if>
<if test="productId != null">product_id = #{productId},</if>
<if test="productName != null">product_name = #{productName},</if>
<if test="quantity != null">quantity = #{quantity},</if>
<if test="outQuantity != null">out_quantity = #{outQuantity},</if>
<if test="jobDetailId != null">job_detail_id = #{jobDetailId},</if>
<if test="stockinDetailId != null">stockin_detail_id = #{stockinDetailId},</if>
update_time = getdate(),
</trim>
where stock_detail_id = #{stockDetailId}
</update>
<delete id="deleteBasStockDetailByStockDetailId" parameterType="String">
delete from bas_stock_detail where stock_detail_id = #{stockDetailId}
</delete>
<delete id="deleteBasStockDetailByStockIds" parameterType="String">
delete from bas_stock_detail where stock_id in
<foreach item="stockId" collection="array" open="(" separator="," close=")">
#{stockId}
</foreach>
</delete>
</mapper>
- Step 4: Create service and controller using existing RuoYi pattern
Controller endpoint must be:
@PreAuthorize("@ss.hasPermi('bas:stock:list')")
@GetMapping("/list")
public TableDataInfo list(BasStockDetail basStockDetail) {
startPage();
List<BasStockDetail> list = basStockDetailService.selectBasStockDetailList(basStockDetail);
return getDataTable(list);
}
- Step 5: Run IDE lints
Run lints on the new package and fix imports/package names until there are no diagnostics.
Task 3: Convert Stock UI To Master/Detail
Files:
-
Create:
ruoyi-ui/src/api/bas/stockDetail.js -
Modify:
ruoyi-ui/src/views/bas/stock/index.vue -
Step 1: Add detail API
import request from '@/utils/request'
export function listStockDetail(query) {
return request({
url: '/bas/stockdetail/list',
method: 'get',
params: query
})
}
- Step 2: Update stock page row-click behavior
In ruoyi-ui/src/views/bas/stock/index.vue, import:
import { listStockDetail } from '@/api/bas/stockDetail'
Add state:
currentStock: null,
stockDetailList: [],
stockDetailLoading: false,
stockDetailTotal: 0,
stockDetailQueryParams: {
pageNum: 1,
pageSize: 10,
stockId: null
}
Add methods:
handleStockRowClick(row) {
this.currentStock = row
this.stockDetailQueryParams.pageNum = 1
this.stockDetailQueryParams.stockId = row.stockId
this.getStockDetailList()
},
getStockDetailList() {
if (!this.stockDetailQueryParams.stockId) {
this.stockDetailList = []
this.stockDetailTotal = 0
return
}
this.stockDetailLoading = true
listStockDetail(this.stockDetailQueryParams).then(response => {
this.stockDetailList = response.rows || []
this.stockDetailTotal = response.total || 0
this.stockDetailLoading = false
}).catch(() => {
this.stockDetailList = []
this.stockDetailTotal = 0
this.stockDetailLoading = false
})
}
- Step 3: Adjust top table to show only master fields
Top table columns should keep:
stockIdcargoSpaceIdstockStatustrayCodestockinIdinTypewarehouseNameuserNamedeptNamesuplierName
Move material columns to the lower detail table:
-
productId -
productName -
lotNo -
quantity -
outQuantity -
jobDetailId -
stockinDetailId -
Step 4: Verify manually
Open inventory page, click a master stock row, and confirm detail rows load from /bas/stockdetail/list?stockId=....
Task 4: Make Inbound Completion Write Stock Master And Detail
Files:
-
Create:
sql/proc_complete_in_job_stock_detail.sql -
Step 1: Replace stock insertion block inside
proc_complete_in_job
Use the current procedure definition from FTC25009-WMS. In the cursor loop, replace the existing single INSERT INTO bas_stock (...) VALUES (...) block with this block:
-- 插入库存主表。主表只保存托盘、货位、仓库、入库来源等通用信息。
IF NOT EXISTS (SELECT 1 FROM dbo.bas_stock WHERE stock_id = @stock_id)
BEGIN
INSERT INTO dbo.bas_stock (
stock_id, cargo_space_id, stock_status, in_date,
job_id, tray_code, stockin_id, in_type, warehouse_id
)
VALUES (
@stock_id, @cargo_space_id, '1', CONVERT(VARCHAR, GETDATE(), 120),
@job_id, @tray_code, @stockin_id, NULL, @warehouse_id
);
END;
-- 插入库存子表。子表保存物料、批次、数量和来源子单。
INSERT INTO dbo.bas_stock_detail (
stock_detail_id, stock_id, product_status, lot_no, product_id, product_name,
quantity, out_quantity, job_detail_id, stockin_detail_id, create_time
)
VALUES (
@stock_id + '-' + CAST(ABS(CHECKSUM(@job_detail_id)) AS varchar(20)),
@stock_id,
'1',
@lot_no,
@product_id,
@product_name,
@in_quantity,
0,
@job_detail_id,
@stockin_detail_id,
GETDATE()
);
- Step 2: Keep legacy fields during this phase
Do not drop old material columns from bas_stock in this task. Existing outbound allocation, stocktake, dashboard, and reports still contain direct references to bas_stock.product_id, bas_stock.quantity, and related fields. Remove those only after all readers are moved to bas_stock_detail.
- Step 3: Apply the procedure script
Run:
powershell -ExecutionPolicy Bypass -File ".\scripts\apply-sql-file.ps1" -SqlFile ".\sql\proc_complete_in_job_stock_detail.sql"
- Step 4: Verify with a controlled inbound completion
After completing one inbound job, run:
SELECT TOP 5 s.stock_id, s.cargo_space_id, s.tray_code, s.warehouse_id,
d.product_id, d.product_name, d.quantity, d.job_detail_id, d.stockin_detail_id
FROM dbo.bas_stock s
INNER JOIN dbo.bas_stock_detail d ON d.stock_id = s.stock_id
ORDER BY d.create_time DESC;
Expected: one master stock row with one or more detail material rows.
Task 5: Fix Completed Inbound Job Detail Lookup
Files:
-
Modify:
ruoyi-admin/src/main/resources/mapper/in/MydoJobDetailMapper.xml -
Modify:
ruoyi-ui/src/views/in/job/index.vue -
Step 1: Fix finished detail SQL
Replace selectMydoJobDetailFinishList with:
<select id="selectMydoJobDetailFinishList" parameterType="MydoJobDetail" resultMap="MydoJobDetailResult">
<include refid="selectMydoJobDetailFinishVo"/>
<where>
<if test="jobId != null and jobId != ''"> and job_id = #{jobId}</if>
<if test="stockId != null and stockId != ''"> and stock_id = #{stockId}</if>
<if test="lotNo != null and lotNo != ''"> and lot_no = #{lotNo}</if>
<if test="inDate != null and inDate != ''"> and in_date = #{inDate}</if>
<if test="inOrOutId != null and inOrOutId != ''"> and in_or_out_id = #{inOrOutId}</if>
<if test="inOrOutDId != null and inOrOutDId != ''"> and in_or_out_d_id = #{inOrOutDId}</if>
<if test="productId != null and productId != ''"> and product_id = #{productId}</if>
<if test="productName != null and productName != ''"> and product_name like concat('%', #{productName}, '%')</if>
<if test="inQuantity != null "> and in_quantity = #{inQuantity}</if>
<if test="stockQuantity != null "> and stock_quantity = #{stockQuantity}</if>
<if test="outQuantity != null "> and out_quantity = #{outQuantity}</if>
<if test="customerName != null and customerName != ''"> and customer_name like concat('%', #{customerName}, '%')</if>
<if test="supplierId != null and supplierId != ''"> and supplier_id = #{supplierId}</if>
<if test="supplierName != null and supplierName != ''"> and supplier_name like concat('%', #{supplierName}, '%')</if>
</where>
</select>
- Step 2: Pass selected row status to child query
In ruoyi-ui/src/views/in/job/index.vue, update handleRowClick(row) so it sets:
this.detailQueryParams.jobId = row.jobId
this.detailQueryParams.status = row.status
Do not use this.queryParams.status for child query routing because the selected row is the source of truth.
- Step 3: Verify active and completed jobs
Use the 入库作业管理 page:
- query active job, click row, child details should load from
mydo_job_detail. - query status
4, click row, child details should load frommydo_job_detail_finish.
Task 6: Create bas_api_log Table And Retention Procedure
Files:
-
Create:
sql/bas_api_log.sql -
Step 1: Add API log table SQL
IF OBJECT_ID(N'dbo.bas_api_log', N'U') IS NULL
BEGIN
CREATE TABLE dbo.bas_api_log (
id bigint IDENTITY(1,1) NOT NULL,
direction varchar(16) NOT NULL,
sys_code varchar(50) NULL,
interface_name nvarchar(100) NULL,
request_url nvarchar(500) NULL,
request_method varchar(20) NULL,
request_body nvarchar(max) NULL,
response_body nvarchar(max) NULL,
msg_type varchar(20) NULL,
http_status int NULL,
cost_ms bigint NULL,
status varchar(30) NULL,
error_msg nvarchar(2000) NULL,
source_ip varchar(80) NULL,
trace_id varchar(80) NULL,
create_time datetime2(7) NOT NULL CONSTRAINT DF_bas_api_log_create_time DEFAULT GETDATE(),
CONSTRAINT PK_bas_api_log PRIMARY KEY CLUSTERED(id)
);
END
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.bas_api_log') AND name = N'IX_bas_api_log_create_time')
CREATE INDEX IX_bas_api_log_create_time ON dbo.bas_api_log(create_time);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.bas_api_log') AND name = N'IX_bas_api_log_direction_sys')
CREATE INDEX IX_bas_api_log_direction_sys ON dbo.bas_api_log(direction, sys_code, create_time DESC);
GO
CREATE OR ALTER PROCEDURE dbo.proc_clean_bas_api_log
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM dbo.bas_api_log
WHERE create_time < DATEADD(DAY, -30, GETDATE());
END
GO
- Step 2: Apply SQL
Run:
powershell -ExecutionPolicy Bypass -File ".\scripts\apply-sql-file.ps1" -SqlFile ".\sql\bas_api_log.sql"
Expected output:
Applied SQL file to FTC25009-WMS
Task 7: Persist API Logs
Files:
-
Create:
ruoyi-common/src/main/java/com/ruoyi/common/annotation/ApiLog.java -
Modify:
ruoyi-admin/src/main/java/com/ruoyi/api/domain/ApiLog.java -
Create:
ruoyi-admin/src/main/java/com/ruoyi/api/mapper/ApiLogMapper.java -
Create:
ruoyi-admin/src/main/resources/mapper/api/ApiLogMapper.xml -
Modify:
ruoyi-admin/src/main/java/com/ruoyi/api/service/ApiLogService.java -
Step 1: Add annotation
package com.ruoyi.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiLog {
String interfaceName() default "";
String sysCode() default "";
String direction() default "INBOUND";
boolean isSaveRequestData() default true;
boolean isSaveResponseData() default true;
String[] excludeParamNames() default {};
}
- Step 2: Add mapper
package com.ruoyi.api.mapper;
import com.ruoyi.api.domain.ApiLog;
public interface ApiLogMapper {
int insertApiLog(ApiLog apiLog);
int deleteExpiredApiLog();
}
- Step 3: Add mapper XML
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.api.mapper.ApiLogMapper">
<insert id="insertApiLog" parameterType="com.ruoyi.api.domain.ApiLog">
insert into bas_api_log(direction, sys_code, interface_name, request_url, request_method,
request_body, response_body, msg_type, http_status, cost_ms, status, error_msg,
source_ip, trace_id, create_time)
values(#{direction}, #{sysCode}, #{interfaceName}, #{requestUrl}, #{requestMethod},
#{requestBody}, #{responseBody}, #{msgType}, #{httpStatus}, #{costMs}, #{status}, #{errorMsg},
#{sourceIp}, #{traceId}, getdate())
</insert>
<delete id="deleteExpiredApiLog">
delete from bas_api_log where create_time < dateadd(day, -30, getdate())
</delete>
</mapper>
- Step 4: Change service to persist
Inject ApiLogMapper into ApiLogService, truncate request/response/error to safe sizes, and call apiLogMapper.insertApiLog(apiLog) in record(ApiLog apiLog).
Concrete core implementation:
@Autowired
private ApiLogMapper apiLogMapper;
public void record(ApiLog apiLog) {
if (apiLog == null) {
return;
}
apiLog.setRequestBody(limit(apiLog.getRequestBody(), MAX_BODY_LEN));
apiLog.setResponseBody(limit(apiLog.getResponseBody(), MAX_BODY_LEN));
apiLog.setErrorMsg(limit(apiLog.getErrorMsg(), 2000));
apiLog.setCreateTime(LocalDateTime.now());
apiLogMapper.insertApiLog(apiLog);
}
public int cleanExpired() {
return apiLogMapper.deleteExpiredApiLog();
}
private String limit(String value, int maxLength) {
if (value == null || value.length() <= maxLength) {
return value;
}
return value.substring(0, maxLength) + "...[truncated]";
}
Task 8: Add API Log Aspect And Retention Task
Files:
-
Create:
ruoyi-admin/src/main/java/com/ruoyi/api/aspect/ApiLogAspect.java -
Create:
ruoyi-admin/src/main/java/com/ruoyi/api/task/ApiLogCleanupTask.java -
Modify:
ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java -
Modify:
ruoyi-admin/src/main/java/com/ruoyi/api/client/BaseExternalClient.java -
Step 1: Add aspect for inbound external API methods
Implement ApiLogAspect in ruoyi-admin rather than ruoyi-framework, because the aspect needs compile-time access to com.ruoyi.api.service.ApiLogService.
The aspect must:
-
start timer in
@Before("@annotation(apiLog)") -
on return, save
status=SUCCESS -
on exception, save
status=FAIL -
fill
direction,sysCode,interfaceName, URL, method, source IP, request JSON, response JSON, error, cost. -
Step 2: Annotate inbound WCS callbacks
In ExternalApiController, annotate at least:
@ApiLog(interfaceName = "WCS-请求入库货位", sysCode = "WCS", direction = "INBOUND")
@PostMapping("/wcs/inbound/location")
public AjaxResult inboundLocation(@RequestBody WcsInboundLocationRequest request) { ... }
@ApiLog(interfaceName = "WCS-完成入库作业", sysCode = "WCS", direction = "INBOUND")
@PostMapping("/wcs/inbound/complete")
public AjaxResult completeInbound(@RequestBody WcsInboundCompleteRequest request) { ... }
- Step 3: Ensure outbound logs write to
bas_api_log
In BaseExternalClient.post(...), keep the existing log construction but set:
apiLog.setDirection("OUTBOUND");
apiLog.setRequestMethod("POST");
apiLog.setSourceIp(null);
apiLogService.record(apiLog);
- Step 4: Add cleanup task
package com.ruoyi.api.task;
import com.ruoyi.api.service.ApiLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ApiLogCleanupTask {
@Autowired
private ApiLogService apiLogService;
@Scheduled(cron = "0 30 2 * * ?")
public void cleanExpiredApiLogs() {
apiLogService.cleanExpired();
}
}
If this project does not enable Spring scheduling, register a RuoYi Quartz job that invokes apiLogCleanupTask.cleanExpiredApiLogs daily at 02:30.
Task 9: Verification
Files:
-
No code files, verification only.
-
Step 1: Verify database schema
Run:
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME IN ('bas_stock', 'bas_stock_detail', 'bas_api_log');
Expected: all three names returned.
- Step 2: Verify completed job detail lookup
Run:
SELECT TOP 1 job_id FROM dbo.mydo_job_finish ORDER BY finish_time DESC;
Then call:
GET /in/jobdetail/list?jobId=<job_id>&status=4
Expected: rows from mydo_job_detail_finish for that job_id.
- Step 3: Verify API log insert
Call WCS inbound location or complete endpoint, then run:
SELECT TOP 10 direction, sys_code, interface_name, status, create_time
FROM dbo.bas_api_log
ORDER BY id DESC;
Expected: INBOUND/WCS rows for WCS callbacks and OUTBOUND/WCS rows for WMS calling WCS.
- Step 4: Run backend build
Run:
mvn -pl ruoyi-admin -am -DskipTests compile
Expected: PASS after Maven is upgraded to at least 3.6.3. Current local environment previously failed before Java compilation because maven-compiler-plugin:3.13.0 requires Maven 3.6.3.
- Step 5: Run frontend build
Run:
npm run build:prod
from ruoyi-ui.
Expected: production bundle builds successfully.
Self-Review
Spec coverage:
- Inventory master/detail database, backend, and frontend are covered in Tasks 1-4.
- Completed inbound job detail lookup is covered in Task 5.
- API log annotation, persistence to
bas_api_log, outbound/inbound coverage, and 30-day retention are covered in Tasks 6-8.
Known implementation guardrail:
- Do not drop material columns from
bas_stockin this iteration. Existing outbound allocation, stocktake, dashboard, and report SQL still read the old flat shape. This plan moves the UI/backend to master/detail while preserving compatibility; a later cleanup can remove old columns after all readers are migrated.
Type consistency:
- Java uses
BasStockDetail.stockDetailId, SQL usesstock_detail_id. - API log Java uses camelCase fields mapping to snake_case columns in MyBatis.