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.timiserverapi TimiServerAPI - 1.0.57 + 1.0.58 jar TimiServerAPI imyeyu.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} +