diff --git a/.serena/project.yml b/.serena/project.yml
index 620976b..0abdfde 100644
--- a/.serena/project.yml
+++ b/.serena/project.yml
@@ -1,34 +1,6 @@
-# the name by which the project can be referenced within Serena
+# the name by which the project can be referenced within Serena/when chatting with the LLM.
project_name: "TimiServerAPI"
-
-# list of languages for which language servers are started; choose from:
-# al bash clojure cpp csharp
-# csharp_omnisharp dart elixir elm erlang
-# fortran fsharp go groovy haskell
-# haxe java julia kotlin lua
-# markdown
-# matlab nix pascal perl php
-# php_phpactor powershell python python_jedi r
-# rego ruby ruby_solargraph rust scala
-# swift terraform toml typescript typescript_vts
-# vue yaml zig
-# (This list may be outdated. For the current list, see values of Language enum here:
-# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
-# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
-# Note:
-# - For C, use cpp
-# - For JavaScript, use typescript
-# - For Free Pascal/Lazarus, use pascal
-# Special requirements:
-# Some languages require additional setup/installations.
-# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
-# When using multiple languages, the first language server that supports a given file will be used for that file.
-# The first language is the default language and the respective language server will be used as a fallback.
-# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
-languages:
-- java
-
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
@@ -50,12 +22,19 @@ ignore_all_files_in_gitignore: true
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
-# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
-# No documentation on options means no options are available.
+# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
+# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
ls_specific_settings: {}
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
+# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
+# Example:
+# ignored_paths:
+# - "examples/**"
+# - ".worktrees/**"
+# - "**/bin/**"
+# - "**/obj/**"
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
@@ -66,54 +45,17 @@ read_only: false
# list of tool names to exclude.
# This extends the existing exclusions (e.g. from the global configuration)
-#
-# Below is the complete list of tools for convenience.
-# To make sure you have the latest list of tools, and to view their descriptions,
-# execute `uv run scripts/print_tool_overview.py`.
-#
-# * `activate_project`: Activates a project based on the project name or path.
-# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
-# * `create_text_file`: Creates/overwrites a file in the project directory.
-# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly,
-# for example by saying that the information retrieved from a memory file is no longer correct
-# or no longer relevant for the project.
-# * `edit_memory`: Replaces content matching a regular expression in a memory.
-# * `execute_shell_command`: Executes a shell command.
-# * `find_file`: Finds files in the given relative paths
-# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend
-# * `find_symbol`: Performs a global (or local) search using the language server backend.
-# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
-# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
-# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual')
-# for clients that do not read the initial instructions when the MCP server is connected.
-# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
-# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
-# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
-# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool.
-# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
-# * `read_file`: Reads a file within the project directory.
-# * `read_memory`: Read the content of a memory file. This tool should only be used if the information
-# is relevant to the current task. You can infer whether the information
-# is relevant from the memory file name.
-# You should not read the same memory file multiple times in the same conversation.
-# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported
-# (e.g., renaming "global/foo" to "bar" moves it from global to project scope).
-# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities.
-# For JB, we use a separate tool.
-# * `replace_content`: Replaces content in a file (optionally using regular expressions).
-# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend.
-# * `safe_delete_symbol`:
-# * `search_for_pattern`: Performs a search for a pattern in the project.
-# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
-# The memory name should be meaningful.
+# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
+# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
+# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
fixed_tools: []
# list of mode names to that are always to be included in the set of active modes
@@ -124,11 +66,14 @@ fixed_tools: []
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:
-# list of mode names that are to be activated by default.
-# The full set of modes to be activated is base_modes + default_modes.
-# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
+# list of mode names that are to be activated by default, overriding the setting in the global configuration.
+# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
+# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
+# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
+# for this project.
# This setting can, in turn, be overridden by CLI parameters (--mode).
+# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
default_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
@@ -152,3 +97,81 @@ read_only_memory_patterns: []
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []
+
+# list of language servers to start when using the LSP backend; choose from:
+# ada al angular ansible bash
+# bsl clojure cpp cpp_ccls crystal
+# csharp csharp_omnisharp cue dart deno
+# elixir elm erlang fortran fsharp
+# gdscript gleam go groovy haskell
+# haxe hlsl html java json
+# julia kotlin latex lean4 lua
+# luau markdown matlab msl nextflow
+# nix ocaml pascal perl php
+# php_phpactor php_phpantom powershell python python_basedpyright
+# python_jedi python_pyrefly python_ty qml r
+# rego ruby ruby_solargraph rust scala
+# scss solidity svelte swift systemverilog
+# terraform toml typescript typescript_vts vue
+# wolfram yaml zig
+# (This list may be outdated; generated with scripts/print_language_list.py;
+# For the current list, see values of the LanguageServerId enum here:
+# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
+# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
+# Note:
+# - For C, use cpp
+# - For JavaScript, use typescript
+# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
+# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
+# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH)
+# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
+# - For Free Pascal/Lazarus, use pascal
+# Special requirements:
+# Some language servers require additional setup/installations.
+# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
+# When using multiple language servers, the first language server that supports a given file will be used for that file.
+# The first language server is the default language and the respective language server will be used as a fallback.
+# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
+language_servers:
+- java
+
+# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
+# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
+# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
+added_modes:
+
+# list of workspace folder paths (LSP backend only).
+# These folders will be used to build up Serena's symbol index.
+# Paths must be within the project root and should thus be relative to the project root.
+# Furthermore, the paths should not be filtered by ignore settings.
+# Default setting: The entire project root folder (".") is considered.
+# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
+# ls_workspace_folders:
+# - "./subproject1"
+# - "./subproject2"
+ls_workspace_folders:
+- .
+
+# list of additional workspace folder paths for cross-package reference support.
+# Paths can be absolute or relative to the project root.
+# Each folder is registered as an LSP workspace folder, enabling language servers to discover
+# symbols and references across package boundaries, but these folders are not indexed by Serena,
+# i.e. the respective symbols will not be found using Serena's symbol search tools.
+# Example:
+# additional_workspace_folders:
+# - ../sibling-package
+# - ../shared-lib
+ls_additional_workspace_folders: []
+
+# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
+# the command runs in the project root directory and is only executed if the project is trusted
+# (see trusted_project_path_patterns in the global configuration).
+# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
+# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
+# backstop for non-terminating commands; on expiry the process is killed and activation continues.
+# example: activation_command: "npx nx run-many -t build"
+activation_command:
+
+# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
+# must be a positive number.
+activation_command_timeout: 180.0
diff --git a/pom.xml b/pom.xml
index 68c61f0..482d009 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
com.imyeyu.timiserverapiTimiServerAPI
- 1.0.57
+ 1.0.58jarTimiServerAPIimyeyu.com API
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointRedeemSkuController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointRedeemSkuController.java
index 3fa56dd..7f583b3 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointRedeemSkuController.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointRedeemSkuController.java
@@ -6,11 +6,11 @@ import com.imyeyu.api.modules.gao.entity.GaoPointRedeemSku;
import com.imyeyu.api.modules.gao.service.GaoPointRedeemSkuService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPage;
+import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPageResult;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuRequest;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
-import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -30,11 +30,15 @@ public class GaoPointRedeemSkuController {
private final GaoPointRedeemSkuService service;
private final GaoStoreScopeService storeScopeService;
+ /// 分页查询兑换 SKU 配置,指定 SKU 时附带基础单位兑换统计
+ /// @param page 分页与筛选条件
+ /// @return 兑换 SKU 分页结果,指定 SKU 时包含基础单位兑换统计
+ /// @throws TimiException 分页条件为空
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_REDEEM_SKU_MANAGE)
@PostMapping("/list")
- public PageResult list(@RequestBody GaoPointRedeemSkuPage page) {
+ public GaoPointRedeemSkuPageResult list(@RequestBody GaoPointRedeemSkuPage page) {
TimiException.required(page, "not found redeem sku page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return service.pageByQuery(page);
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java
index 0bf0849..cfc82bb 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java
@@ -9,10 +9,12 @@ import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductStockBatchRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
+import com.imyeyu.api.modules.gao.vo.GaoProductStockManualInBatchRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView;
import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage;
+import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPageResult;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
@@ -93,6 +95,21 @@ public class GaoProductStockController {
return stockService.manualIn(normalize(request));
}
+ /// 批量手工入库
+ ///
+ /// @param request 批量手工入库请求
+ /// @return 更新后的库存列表
+ /// @throws TimiException 请求或库存校验失败
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE)
+ @PostMapping("/manual-in/batch")
+ public List manualInBatch(@RequestBody GaoProductStockManualInBatchRequest request) {
+ TimiException.required(request, "not found stock batch request");
+ request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId()));
+ return stockService.manualInBatch(request);
+ }
+
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE)
@@ -125,11 +142,15 @@ public class GaoProductStockController {
return stockService.stocktakeLoss(normalize(request));
}
+ /// 按条件分页查询库存流水并汇总 SKU 基础单位变动
+ /// @param page 流水分页与筛选条件
+ /// @return 流水分页及匹配条件下的 SKU 基础单位汇总
+ /// @throws TimiException 分页请求为空或门店范围校验失败
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ)
@PostMapping("/record/list")
- public PageResult recordList(@RequestBody GaoProductStockRecordPage page) {
+ public GaoProductStockRecordPageResult recordList(@RequestBody GaoProductStockRecordPage page) {
TimiException.required(page, "not found stock record page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return recordService.pageByQuery(page);
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRedemptionMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRedemptionMapper.java
index e75f5e0..d2c9e3f 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRedemptionMapper.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRedemptionMapper.java
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoPointRedemption;
import com.imyeyu.api.modules.gao.vo.GaoPointRedemptionPage;
+import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
@@ -32,4 +33,12 @@ public interface GaoPointRedemptionMapper extends BaseMapper selectByQuery(GaoProductStockRecordPage page);
+
+ /// 按条件统计指定 SKU 的出入库基础单位数量,不受分页字段限制
+ /// @param page 包含 SKU 和库存流水筛选条件的请求
+ /// @return 指定 SKU 的出入库基础单位数量
+ GaoProductStockRecordSkuSummaryView selectSkuBaseUnitSummaryByQuery(GaoProductStockRecordPage page);
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRedeemSkuService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRedeemSkuService.java
index de8f1ff..5022713 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRedeemSkuService.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRedeemSkuService.java
@@ -2,8 +2,8 @@ package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoPointRedeemSku;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPage;
+import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPageResult;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuRequest;
-import com.imyeyu.spring.bean.PageResult;
/// GAO 积分兑换 SKU 配置服务
///
@@ -11,8 +11,11 @@ import com.imyeyu.spring.bean.PageResult;
/// @since 2026-09-14
public interface GaoPointRedeemSkuService {
- /// 分页查询兑换 SKU 配置
- PageResult pageByQuery(GaoPointRedeemSkuPage page);
+ /// 分页查询兑换 SKU 配置,指定 SKU 时附带基础单位兑换统计
+ /// @param page 分页与筛选条件
+ /// @return 兑换 SKU 分页结果,指定 SKU 时包含基础单位兑换统计
+ /// @throws TimiException 分页条件为空
+ GaoPointRedeemSkuPageResult pageByQuery(GaoPointRedeemSkuPage page);
/// 查询兑换 SKU 配置详情
GaoPointRedeemSku detail(String storeId, String id);
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java
index 3686785..ab2beb1 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java
@@ -3,7 +3,7 @@ package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord;
import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage;
-import com.imyeyu.spring.bean.PageResult;
+import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPageResult;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
@@ -14,8 +14,10 @@ import java.util.List;
/// @since 2026-09-02
public interface GaoProductStockRecordService extends BaseService {
- /// 按条件查询库存流水
- PageResult pageByQuery(GaoProductStockRecordPage page);
+ /// 按条件查询库存流水;指定 SKU 时统计其出入库基础单位数量
+ /// @param page 流水分页和统计筛选条件
+ /// @return 流水分页及匹配条件下的 SKU 基础单位汇总
+ GaoProductStockRecordPageResult pageByQuery(GaoProductStockRecordPage page);
/// 查询库存流水详情
GaoProductStockRecord detail(String id);
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java
index 2c4952f..64b68c4 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java
@@ -4,10 +4,12 @@ import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.gao.entity.GaoProductStock;
import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord;
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
+import com.imyeyu.api.modules.gao.vo.GaoProductStockManualInBatchRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView;
import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem;
+import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.PageResult;
import java.util.List;
@@ -36,6 +38,13 @@ public interface GaoProductStockService {
/// 手工入库
GaoProductStock manualIn(GaoProductStockChangeRequest request);
+ /// 批量手工入库
+ ///
+ /// @param request 批量手工入库请求
+ /// @return 更新后的库存列表
+ /// @throws TimiException 请求或库存校验失败
+ List manualInBatch(GaoProductStockManualInBatchRequest request);
+
/// 手工出库
GaoProductStock manualOut(GaoProductStockChangeRequest request);
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java
index b4c032f..5e25797 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java
@@ -168,18 +168,19 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService requestItems = filterZeroQuantityItems(request.getItemList());
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
TimiException.required(request.getSalesOrderId(), "not found salesOrderId");
GaoSalesOrder order = salesOrderService.getForUpdate(request.getSalesOrderId());
GaoOrderFulfillment existing = mapper.selectByRequestNo(order.getStoreId(), requestNo);
if (existing != null) {
- verifyIdempotency(existing, request);
+ verifyIdempotency(existing, request, requestItems);
return detail(existing.getId());
}
TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "履约必须上传照片");
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能履约");
salesOrderService.checkFulfillmentPermission(order);
- List sourceList = loadSources(order, request.getItemList(), true);
+ List sourceList = loadSources(order, requestItems, true);
List stockItems = buildStockItems(order.getStoreId(), sourceList);
boolean stockReady = stockItems.isEmpty() || productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems);
String operatorUserId = userLoginService.getRequireLoginUserId();
@@ -443,6 +444,19 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService filterZeroQuantityItems(List requestItems) {
+ if (requestItems == null) {
+ return null;
+ }
+ return requestItems.stream()
+ .filter(item -> item == null || item.getQuantity() == null || 0 != item.getQuantity().signum())
+ .toList();
+ }
+
private List loadSources(GaoSalesOrder order, List requestItems, boolean checkAvailable) {
TimiException.requiredTrue(requestItems != null && !requestItems.isEmpty(), "履约至少需要一个产品");
List sortedRequestItems = new ArrayList<>(requestItems);
@@ -657,12 +671,12 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService requestItems) {
TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId())
&& (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt()))
&& sameText(existing.getRemark(), request.getRemark()), "requestNo 对应履约请求内容不一致");
List existingItems = itemMapper.selectByFulfillmentId(existing.getId());
- List requestItems = request.getItemList();
TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致");
Map requestMap = new HashMap<>();
for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) {
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java
index 70df61c..487d284 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java
@@ -237,8 +237,12 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
TimiException.required(request, "not found fund revoke request");
String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
String reason = requiredText(request.getReason(), "资金冲正原因不能为空");
+ GaoFundRecord sourceSnapshot = fundRecordService.detail(request.getFundRecordId());
+ TimiException.requiredTrue(sourceSnapshot.getBizType() == GaoFundRecord.BizType.SALES_ORDER, "资金记录不是销售订单资金");
+ TimiException.required(sourceSnapshot.getBizId(), "not found salesOrderId");
+ GaoSalesOrder order = orderService.getForUpdate(sourceSnapshot.getBizId());
GaoFundRecord source = lockOrderRecord(request.getFundRecordId());
- GaoSalesOrder order = orderService.getForUpdate(source.getBizId());
+ TimiException.requiredTrue(order.getId().equals(source.getBizId()), "资金流水所属订单已变化");
GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo);
if (existing != null) {
TimiException.requiredTrue(existing.getOperationType() == GaoFundRecord.OperationType.REVOKE && source.getId().equals(existing.getSourceRecordId()), "requestNo 已被其他资金操作使用");
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRedeemSkuServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRedeemSkuServiceImplement.java
index 06bed54..8b1f965 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRedeemSkuServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRedeemSkuServiceImplement.java
@@ -5,16 +5,17 @@ import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductSku;
import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit;
import com.imyeyu.api.modules.gao.entity.GaoPointRedeemSku;
+import com.imyeyu.api.modules.gao.mapper.GaoPointRedemptionMapper;
import com.imyeyu.api.modules.gao.mapper.GaoPointRedeemSkuMapper;
import com.imyeyu.api.modules.gao.service.GaoPointRedeemSkuService;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductSkuService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPage;
+import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuPageResult;
import com.imyeyu.api.modules.gao.vo.GaoPointRedeemSkuRequest;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
-import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -35,15 +36,24 @@ import java.util.UUID;
public class GaoPointRedeemSkuServiceImplement implements GaoPointRedeemSkuService {
private final GaoPointRedeemSkuMapper mapper;
+ private final GaoPointRedemptionMapper redemptionMapper;
private final GaoProductService productService;
private final GaoProductSkuService skuService;
private final GaoStoreScopeService storeScopeService;
@Override
- public PageResult pageByQuery(GaoPointRedeemSkuPage page) {
+ public GaoPointRedeemSkuPageResult pageByQuery(GaoPointRedeemSkuPage page) {
TimiException.required(page, "not found redeem sku page");
- PageResult result = new PageResult<>();
+ GaoPointRedeemSkuPageResult result = new GaoPointRedeemSkuPageResult();
result.setTotal(mapper.countByQuery(page));
+ if (page.getSkuId() != null && !page.getSkuId().isBlank()) {
+ GaoPointRedeemSkuPageResult summary = redemptionMapper.selectSkuSummaryByStoreIdAndSkuId(
+ page.getStoreId(), page.getSkuId()
+ );
+ result.setRedeemedBaseQuantityTotal(summary.getRedeemedBaseQuantityTotal());
+ result.setReturnedBaseQuantityTotal(summary.getReturnedBaseQuantityTotal());
+ result.setNetRedeemedBaseQuantityTotal(summary.getNetRedeemedBaseQuantityTotal());
+ }
List list = mapper.selectByQuery(page);
fillTransient(list);
result.setList(list);
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java
index c83f25b..bc55a4e 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java
@@ -10,12 +10,13 @@ import com.imyeyu.api.modules.gao.mapper.GaoProductStockRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService;
import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage;
+import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPageResult;
+import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordSkuSummaryView;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
-import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
@@ -45,12 +46,17 @@ public class GaoProductStockRecordServiceImplement extends AbstractEntityService
}
@Override
- public PageResult pageByQuery(GaoProductStockRecordPage page) {
+ public GaoProductStockRecordPageResult pageByQuery(GaoProductStockRecordPage page) {
TimiException.required(page, "not found stock record page");
- PageResult result = new PageResult<>();
+ GaoProductStockRecordPageResult result = new GaoProductStockRecordPageResult();
result.setTotal(mapper.countByQuery(page));
result.setList(mapper.selectByQuery(page));
fillTransient(result.getList());
+ if (page.getSkuId() != null && !page.getSkuId().isBlank()) {
+ GaoProductStockRecordSkuSummaryView summary = mapper.selectSkuBaseUnitSummaryByQuery(page);
+ result.setInBaseQuantityTotal(summary.getInBaseQuantityTotal());
+ result.setOutBaseQuantityTotal(summary.getOutBaseQuantityTotal());
+ }
return result;
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java
index 5a077bb..f131862 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java
@@ -16,6 +16,7 @@ import com.imyeyu.api.modules.gao.service.GaoProductStockService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection;
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
+import com.imyeyu.api.modules.gao.vo.GaoProductStockManualInBatchRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView;
@@ -150,6 +151,11 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
return change(request, GaoProductStockRecord.OperationType.INITIAL_IN);
}
+ /// 批量手工入库
+ ///
+ /// @param request 批量手工入库请求
+ /// @return 更新后的库存列表
+ /// @throws TimiException 请求或库存校验失败
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_MANUAL_IN)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
@@ -157,6 +163,56 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
return change(request, GaoProductStockRecord.OperationType.MANUAL_IN);
}
+ @OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_MANUAL_IN)
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public List manualInBatch(GaoProductStockManualInBatchRequest request) {
+ TimiException.required(request, "not found stock batch request");
+ String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
+ String storeId = storeScopeService.resolveStoreId(request.getStoreId());
+ TimiException.required(storeId, "not found storeId");
+ String remark = request.getRemark();
+ TimiException.requiredTrue(hasText(remark), "库存操作必须填写备注");
+ TimiException.requiredTrue(request.getItemList() != null && !request.getItemList().isEmpty(), "批量入库至少需要一个 SKU");
+ List requestItemList = new ArrayList<>(request.getItemList());
+ requestItemList.removeIf(item -> item != null && item.getQuantity() != null && item.getQuantity().signum() == 0);
+ TimiException.requiredTrue(!requestItemList.isEmpty(), "批量入库至少需要一个正数量 SKU");
+ for (GaoProductStockManualInBatchRequest.Item item : requestItemList) {
+ TimiException.required(item, "库存变更项不能为空");
+ item.setSkuId(requiredText(item.getSkuId(), "not found skuId"));
+ item.setSkuUnitId(requiredText(item.getSkuUnitId(), "not found skuUnitId"));
+ }
+ requestItemList.sort(Comparator.comparing(GaoProductStockManualInBatchRequest.Item::getSkuId));
+ Set skuIdSet = new HashSet<>();
+ List changeItemList = new ArrayList<>();
+ for (GaoProductStockManualInBatchRequest.Item item : requestItemList) {
+ TimiException.requiredTrue(skuIdSet.add(item.getSkuId()), "同一库存操作不能重复 SKU");
+ GaoProductSkuUnitSelection selection = skuService.getSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId(), true);
+ GaoProductSku sku = selection.getSku();
+ GaoProduct product = selection.getProduct();
+ GaoProductSkuUnit unit = selection.getUnit();
+ TimiException.requiredTrue(Boolean.TRUE.equals(sku.getStockEnabled()), "该 SKU 未启用库存管理");
+ BigDecimal quantity = validateQuantity(sku, item.getQuantity());
+ BigDecimal baseQuantity = quantity.multiply(unit.getBaseQuantityPerUnit());
+ GaoStockChangeItem changeItem = GaoStockChangeItem.of(product, sku, unit, quantity, baseQuantity);
+ changeItem.setSkuSpecSnapshot(skuService.getSkuSpecSnapshot(sku));
+ changeItemList.add(changeItem);
+ }
+ List existingList = recordService.listByRequestNo(storeId, requestNo);
+ if (!existingList.isEmpty()) {
+ verifyExistingManualInBatch(existingList, changeItemList, remark);
+ List result = new ArrayList<>();
+ for (GaoStockChangeItem item : changeItemList) {
+ result.add(detail(storeId, item.getSkuId()));
+ }
+ return result;
+ }
+ Map> itemAttachmentMap = prepareManualInBatchAttachments(requestItemList, request.getAttachmentList());
+ return executeInternal(storeId, changeItemList.getFirst().getSkuId(), changeItemList, requestNo,
+ userLoginService.getRequireLoginUserId(), GaoProductStockRecord.OperationType.MANUAL_IN, Time.now(), remark, null,
+ GaoProductStockRecord.BizType.PRODUCT_STOCK, itemAttachmentMap, true);
+ }
+
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_MANUAL_OUT)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
@@ -391,6 +447,13 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
private List executeInternal(String storeId, String bizId, List sourceList, String requestNo,
String operatorUserId, GaoProductStockRecord.OperationType operationType, Long occurredAt, String remark, List attachmentList,
GaoProductStockRecord.BizType bizType) {
+ return executeInternal(storeId, bizId, sourceList, requestNo, operatorUserId, operationType, occurredAt, remark, attachmentList,
+ bizType, null, false);
+ }
+
+ private List executeInternal(String storeId, String bizId, List sourceList, String requestNo,
+ String operatorUserId, GaoProductStockRecord.OperationType operationType, Long occurredAt, String remark, List attachmentList,
+ GaoProductStockRecord.BizType bizType, Map> itemAttachmentMap, boolean skuAsBizId) {
TimiException.required(storeId, "not found storeId");
TimiException.required(bizId, "not found stock bizId");
TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "库存变更至少需要一个产品");
@@ -407,7 +470,11 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
}
List existingList = recordService.listByRequestNo(storeId, normalizedRequestNo);
if (!existingList.isEmpty()) {
- verifyExistingList(existingList, itemList, operationType, bizId);
+ if (skuAsBizId) {
+ verifyExistingManualInBatch(existingList, itemList, remark);
+ } else {
+ verifyExistingList(existingList, itemList, operationType, bizId);
+ }
List result = new ArrayList<>();
for (GaoStockChangeItem item : itemList) {
result.add(detail(storeId, item.getSkuId()));
@@ -447,14 +514,16 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
record.setReservedBefore(reservedBefore);
record.setReservedAfter(reservedAfter);
record.setBizType(bizType);
- record.setBizId(bizId);
+ record.setBizId(skuAsBizId ? item.getSkuId() : bizId);
record.setSourceRecordId(item.getSourceRecordId());
record.setRequestNo(normalizedRequestNo);
record.setOperatorUserId(operatorUserId);
- record.setOccurredAt(TimiJava.defaultIfNull(occurredAt, Time.now()));
+ record.setOccurredAt(occurredAt == null ? Time.now() : occurredAt);
record.setRemark(remark);
+ List recordAttachmentList = itemAttachmentMap == null
+ ? attachmentList : itemAttachmentMap.get(item.getSkuId());
recordService.createOperationRecord(record,
- operationType == GaoProductStockRecord.OperationType.ORDER_OUT ? null : attachmentList);
+ operationType == GaoProductStockRecord.OperationType.ORDER_OUT ? null : recordAttachmentList);
if (operationType == GaoProductStockRecord.OperationType.ORDER_OUT) {
copyAttachments(record.getId(), attachmentList);
}
@@ -562,6 +631,74 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
}
}
+ /// 校验批量手工入库幂等请求
+ ///
+ /// @param existingList 已存在的库存流水
+ /// @param itemList 请求中的入库项
+ /// @param remark 批次备注
+ /// @throws TimiException 请求内容与已存在流水不一致
+ private void verifyExistingManualInBatch(List existingList, List itemList, String remark) {
+ TimiException.requiredTrue(existingList.size() == itemList.size(), "requestNo 对应库存操作内容不一致");
+ Map recordMap = new HashMap<>();
+ for (GaoProductStockRecord record : existingList) {
+ TimiException.requiredTrue(record.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_IN
+ && GaoProductStockRecord.BizType.PRODUCT_STOCK == record.getBizType()
+ && record.getSkuId() != null && record.getSkuId().equals(record.getBizId()), "requestNo 已被其他库存操作使用");
+ TimiException.requiredTrue(recordMap.put(record.getSkuId(), record) == null, "requestNo 对应库存操作内容不一致");
+ }
+ TimiException.requiredTrue(recordMap.size() == itemList.size(), "requestNo 对应库存操作内容不一致");
+ for (GaoStockChangeItem item : itemList) {
+ GaoProductStockRecord record = recordMap.get(item.getSkuId());
+ TimiException.required(record, "requestNo 对应库存操作内容不一致");
+ TimiException.requiredTrue(item.getSkuUnitId().equals(record.getSkuUnitId())
+ && record.getQuantity() != null && item.getQuantity().compareTo(record.getQuantity()) == 0
+ && record.getBaseQuantity() != null && item.getBaseQuantity().compareTo(record.getBaseQuantity()) == 0
+ && sameText(record.getRemark(), remark), "requestNo 对应库存操作内容不一致");
+ }
+ }
+
+ /// 准备批量入库附件并复制复用的全局临时附件
+ ///
+ /// @param itemList 排序后的入库项
+ /// @param globalAttachmentList 全局附件列表
+ /// @return 按 SKU 分组的临时附件列表
+ /// @throws TimiException 附件无效或缺失
+ private Map> prepareManualInBatchAttachments(List itemList,
+ List globalAttachmentList) {
+ Map> result = new HashMap<>();
+ Set usedTempFileIdSet = new HashSet<>();
+ for (GaoProductStockManualInBatchRequest.Item item : itemList) {
+ boolean useGlobalAttachments = item.getAttachmentList() == null || item.getAttachmentList().isEmpty();
+ List sourceAttachmentList = useGlobalAttachments ? globalAttachmentList : item.getAttachmentList();
+ TimiException.requiredTrue(sourceAttachmentList != null && !sourceAttachmentList.isEmpty(), "每个 SKU 至少需要一个附件");
+ List attachmentList = new ArrayList<>();
+ for (Attachment source : sourceAttachmentList) {
+ TimiException.required(source, "入库附件不能为空");
+ String tempFileId = requiredText(source.getTempFileId(), "入库附件 tempFileId 不能为空");
+ Attachment tempFile = attachmentService.get(tempFileId);
+ TimiException.required(tempFile, "not found temp file");
+ TimiException.requiredTrue(tempFile.getBizType() == Attachment.BizType.TEMP_FILE, "入库附件必须是临时文件");
+ if (!usedTempFileIdSet.add(tempFileId)) {
+ TimiException.requiredTrue(useGlobalAttachments, "同一临时附件不能用于多个入库项");
+ Attachment clone = attachmentService.clone(tempFileId);
+ TimiException.required(clone, "not found cloned temp file");
+ TimiException.requiredTrue(clone.getBizType() == Attachment.BizType.TEMP_FILE, "克隆附件必须是临时文件");
+ tempFileId = requiredText(clone.getId(), "克隆附件 ID 不能为空");
+ TimiException.requiredTrue(usedTempFileIdSet.add(tempFileId), "克隆附件 ID 已被使用");
+ }
+ Attachment attachment = new Attachment();
+ attachment.setTempFileId(tempFileId);
+ attachment.setAttachType(source.getAttachType());
+ attachment.setName(source.getName());
+ attachment.setMetadata(source.getMetadata());
+ attachmentList.add(attachment);
+ }
+ TimiException.requiredTrue(!attachmentList.isEmpty(), "每个 SKU 至少需要一个附件");
+ result.put(item.getSkuId(), attachmentList);
+ }
+ return result;
+ }
+
private void fillBalance(GaoProductStock stock) {
stock.setOnHandQuantity(decimal(stock.getOnHandQuantity()));
stock.setReservedQuantity(decimal(stock.getReservedQuantity()));
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java
index c726397..566cc88 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java
@@ -9,6 +9,7 @@ import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
+import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment;
import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductSku;
import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit;
@@ -21,6 +22,7 @@ import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService;
+import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
import com.imyeyu.api.modules.gao.service.GaoOrderStraightThroughService;
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
import com.imyeyu.api.modules.gao.service.GaoProductService;
@@ -35,6 +37,8 @@ import com.imyeyu.api.modules.gao.vo.GaoCustomerProductCustomerView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerProductPage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerProductSummaryView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerProductView;
+import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest;
+import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderItemRequest;
@@ -88,6 +92,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService fulfillmentServiceProvider;
+ private final ObjectProvider paymentServiceProvider;
private final ObjectProvider straightThroughServiceProvider;
private final GaoCustomerService customerService;
private final GaoProductService productService;
@@ -326,7 +331,54 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService fulfillmentRevokeList = request.getFulfillmentRevokeList();
+ Set fulfillmentIdSet = new HashSet<>();
+ Set selectedFulfillmentIdSet = new HashSet<>();
+ if (fulfillmentRevokeList != null && !fulfillmentRevokeList.isEmpty()) {
+ TimiException.requiredTrue(permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.ORDER_FULFILLMENT_REVOKE.getValue()), "缺少履约冲正权限");
+ for (GaoOrderFulfillment fulfillment : fulfillmentService.listBySalesOrderId(order.getId())) {
+ fulfillmentIdSet.add(fulfillment.getId());
+ }
+ for (GaoOrderFulfillmentRevokeRequest revokeRequest : fulfillmentRevokeList) {
+ TimiException.required(revokeRequest, "履约冲正请求不能为空");
+ TimiException.required(revokeRequest.getFulfillmentId(), "not found fulfillmentId");
+ TimiException.requiredTrue(fulfillmentIdSet.contains(revokeRequest.getFulfillmentId()), "履约单不属于当前订单");
+ TimiException.requiredTrue(selectedFulfillmentIdSet.add(revokeRequest.getFulfillmentId()), "履约单不能重复冲正");
+ }
+ }
+
+ List fundRevokeList = request.getFundRevokeList();
+ Set fundRecordIdSet = new HashSet<>();
+ Set selectedFundRecordIdSet = new HashSet<>();
+ if (fundRevokeList != null && !fundRevokeList.isEmpty()) {
+ TimiException.requiredTrue(permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.FUND_REVOKE.getValue()), "缺少资金流水冲正权限");
+ for (GaoFundRecord fundRecord : fundRecordService.listByBizId(order.getId())) {
+ fundRecordIdSet.add(fundRecord.getId());
+ }
+ for (GaoOrderPaymentRevokeRequest revokeRequest : fundRevokeList) {
+ TimiException.required(revokeRequest, "资金冲正请求不能为空");
+ TimiException.required(revokeRequest.getFundRecordId(), "not found fundRecordId");
+ TimiException.requiredTrue(fundRecordIdSet.contains(revokeRequest.getFundRecordId()), "资金流水不属于当前订单");
+ TimiException.requiredTrue(selectedFundRecordIdSet.add(revokeRequest.getFundRecordId()), "资金流水不能重复冲正");
+ }
+ }
+
+ if (fulfillmentRevokeList != null) {
+ for (GaoOrderFulfillmentRevokeRequest revokeRequest : fulfillmentRevokeList) {
+ fulfillmentService.revoke(revokeRequest);
+ }
+ }
+ if (fundRevokeList != null && !fundRevokeList.isEmpty()) {
+ GaoOrderPaymentService paymentService = paymentServiceProvider.getObject();
+ for (GaoOrderPaymentRevokeRequest revokeRequest : fundRevokeList) {
+ paymentService.revoke(revokeRequest);
+ }
+ }
+
+ order = getForUpdate(order.getId());
String userId = userLoginService.getRequireLoginUserId();
GaoSalesOrder.RefundStatus refundStatus = safe(order.getPaidAmount()) == 0 ? GaoSalesOrder.RefundStatus.REFUNDED : GaoSalesOrder.RefundStatus.PENDING;
Long cancelledAt = Time.now();
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPointRedeemSkuPageResult.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPointRedeemSkuPageResult.java
new file mode 100644
index 0000000..188a416
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPointRedeemSkuPageResult.java
@@ -0,0 +1,26 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.imyeyu.api.modules.gao.entity.GaoPointRedeemSku;
+import com.imyeyu.spring.bean.PageResult;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/// GAO 积分兑换 SKU 配置分页查询结果
+///
+/// @author Codex
+/// @since 2026-10-15
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoPointRedeemSkuPageResult extends PageResult {
+
+ /// SKU 筛选条件下的累计兑换基础数量
+ private BigDecimal redeemedBaseQuantityTotal;
+
+ /// SKU 筛选条件下的累计退货基础数量
+ private BigDecimal returnedBaseQuantityTotal;
+
+ /// SKU 筛选条件下的净兑换基础数量
+ private BigDecimal netRedeemedBaseQuantityTotal;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockManualInBatchRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockManualInBatchRequest.java
new file mode 100644
index 0000000..d2fd2ba
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockManualInBatchRequest.java
@@ -0,0 +1,47 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.imyeyu.api.modules.common.entity.Attachment;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/// GAO 产品批量手工入库请求
+///
+/// @author Codex
+/// @since 2026-10-13
+@Data
+public class GaoProductStockManualInBatchRequest {
+
+ /// 客户端幂等请求号
+ private String requestNo;
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 批次备注
+ private String remark;
+
+ /// 全局附件列表
+ private List attachmentList;
+
+ /// 批量入库项
+ private List itemList;
+
+ /// 批量手工入库项
+ @Data
+ public static class Item {
+
+ /// SKU ID
+ private String skuId;
+
+ /// SKU 单位 ID
+ private String skuUnitId;
+
+ /// 入库数量
+ private BigDecimal quantity;
+
+ /// SKU 自有附件列表
+ private List attachmentList;
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPageResult.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPageResult.java
new file mode 100644
index 0000000..bfec4ff
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPageResult.java
@@ -0,0 +1,26 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord;
+import com.imyeyu.spring.bean.PageResult;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/// GAO 库存流水分页查询结果
+///
+/// @author Codex
+/// @since 2026-10-16
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GaoProductStockRecordPageResult extends PageResult {
+
+ /// 指定 SKU 在查询条件下的入库基础单位数量合计
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private BigDecimal inBaseQuantityTotal;
+
+ /// 指定 SKU 在查询条件下的出库基础单位数量合计
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private BigDecimal outBaseQuantityTotal;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordSkuSummaryView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordSkuSummaryView.java
new file mode 100644
index 0000000..6d4790e
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordSkuSummaryView.java
@@ -0,0 +1,19 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/// 库存流水 SKU 基础单位统计
+///
+/// @author Codex
+/// @since 2026-10-16
+@Data
+public class GaoProductStockRecordSkuSummaryView {
+
+ /// 入库基础单位数量合计
+ private BigDecimal inBaseQuantityTotal;
+
+ /// 出库基础单位数量合计
+ private BigDecimal outBaseQuantityTotal;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java
index 73110d6..e1f02f3 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java
@@ -2,6 +2,8 @@ package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
+import java.util.List;
+
/// GAO 销售订单取消请求
///
/// @author Codex
@@ -17,4 +19,10 @@ public class GaoSalesOrderCancelRequest {
/// 幂等请求号
private String requestNo;
+
+ /// 取消时冲正的已核销履约单,按整张履约单冲正
+ private List fulfillmentRevokeList;
+
+ /// 取消时冲正的资金流水,按整条流水冲正
+ private List fundRevokeList;
}
diff --git a/src/main/resources/mapper/timi-server/gao/GaoPointRedemptionMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPointRedemptionMapper.xml
index 6e51f3c..f333a99 100644
--- a/src/main/resources/mapper/timi-server/gao/GaoPointRedemptionMapper.xml
+++ b/src/main/resources/mapper/timi-server/gao/GaoPointRedemptionMapper.xml
@@ -63,4 +63,19 @@
AND `redeemed_at` >= #{beginAt}AND `redeemed_at` <= #{endAt}
+
diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml
index e559371..2e6efef 100644
--- a/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml
+++ b/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml
@@ -31,7 +31,7 @@
1 = 1
AND r.`store_id` = #{storeId}AND k.`product_id` = #{productId}
- AND r.`sku_id` = #{skuId}
+ AND r.`sku_id` = #{skuId}AND (r.`product_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`name_pinyin_full` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_initial` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%') OR p.`code` LIKE CONCAT('%', #{keyword}, '%'))AND r.`biz_type` = #{bizType}AND r.`biz_id` = #{bizId}
@@ -61,4 +61,12 @@
ORDER BY r.`occurred_at` DESC, r.`id` DESC
LIMIT #{offset}, #{limit}
+