diff --git a/pom.xml b/pom.xml
index 07b873a..17e130f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
com.imyeyu.timiserverapi
TimiServerAPI
- 1.0.37
+ 1.0.38
jar
TimiServerAPI
imyeyu.com API
diff --git a/src/main/java/com/imyeyu/api/bean/CorePermissionCode.java b/src/main/java/com/imyeyu/api/bean/CorePermissionCode.java
index 40d966a..2c843a3 100644
--- a/src/main/java/com/imyeyu/api/bean/CorePermissionCode.java
+++ b/src/main/java/com/imyeyu/api/bean/CorePermissionCode.java
@@ -45,6 +45,11 @@ public enum CorePermissionCode implements BuiltinPermissionCode {
TAG_UPDATE("TAG:UPDATE", "标签修改", false, true),
TAG_DELETE("TAG:DELETE", "标签删除", false, true),
+ ARTICLE_CREATE("ARTICLE:CREATE", "文章创建", false, true),
+ ARTICLE_READ("ARTICLE:READ", "文章读取", false, true),
+ ARTICLE_UPDATE("ARTICLE:UPDATE", "文章修改", false, true),
+ ARTICLE_DELETE("ARTICLE:DELETE", "文章删除", false, true),
+
USER_CREATE("USER:CREATE", "用户创建", false, true),
USER_READ("USER:READ", "用户读取", false, true),
USER_UPDATE("USER:UPDATE", "用户修改", false, true),
diff --git a/src/main/java/com/imyeyu/api/modules/common/controller/ArticleController.java b/src/main/java/com/imyeyu/api/modules/common/controller/ArticleController.java
new file mode 100644
index 0000000..b7a91dd
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/common/controller/ArticleController.java
@@ -0,0 +1,95 @@
+package com.imyeyu.api.modules.common.controller;
+
+import com.fasterxml.jackson.annotation.JsonView;
+import com.imyeyu.api.annotation.RequireCorePermission;
+import com.imyeyu.api.bean.CorePermissionCode;
+import com.imyeyu.api.modules.common.entity.Article;
+import com.imyeyu.api.modules.common.entity.Attachment;
+import com.imyeyu.api.modules.common.service.ArticleService;
+import com.imyeyu.api.modules.common.service.AttachmentService;
+import com.imyeyu.spring.annotation.AOPLog;
+import com.imyeyu.spring.annotation.RequestRateLimit;
+import com.imyeyu.spring.bean.Page;
+import com.imyeyu.spring.bean.PageResult;
+import com.imyeyu.spring.util.ResponseView;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/// 文章接口
+///
+/// @author Codex
+/// @since 2026-09-09
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/article")
+public class ArticleController {
+
+ private final ArticleService service;
+ private final AttachmentService attachmentService;
+
+ /// 查询文章列表
+ ///
+ /// @param page 分页参数
+ /// @return 文章分页
+ @JsonView(ResponseView.Public.class)
+ @RequestRateLimit
+ @PostMapping("/list")
+ public PageResult list(@RequestBody Page page) {
+ return service.page(page);
+ }
+
+ /// 查询文章详情
+ ///
+ /// @param id 文章 ID
+ /// @return 文章详情
+ @JsonView(ResponseView.Public.class)
+ @RequestRateLimit
+ @PostMapping("/detail")
+ public Article detail(@RequestParam String id) {
+ Article article = service.get(id);
+ article.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.ARTICLE, article.getId()));
+ return article;
+ }
+
+ /// 创建文章
+ ///
+ /// @param article 文章
+ /// @return 创建后的文章
+ @AOPLog
+ @JsonView(ResponseView.Public.class)
+ @RequestRateLimit
+ @RequireCorePermission(CorePermissionCode.ARTICLE_CREATE)
+ @PostMapping("/create")
+ public Article create(@RequestBody Article article) {
+ service.create(article);
+ return article;
+ }
+
+ /// 更新文章
+ ///
+ /// @param article 文章
+ @AOPLog
+ @RequestRateLimit
+ @RequireCorePermission(CorePermissionCode.ARTICLE_UPDATE)
+ @PostMapping("/update")
+ public void update(@RequestBody Article article) {
+ service.update(article);
+ }
+
+ /// 删除文章
+ ///
+ /// @param id 文章 ID
+ @AOPLog
+ @RequestRateLimit
+ @RequireCorePermission(CorePermissionCode.ARTICLE_DELETE)
+ @PostMapping("/delete")
+ public void delete(@RequestParam String id) {
+ service.delete(id);
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/Article.java b/src/main/java/com/imyeyu/api/modules/common/entity/Article.java
index c040df1..b268e5c 100644
--- a/src/main/java/com/imyeyu/api/modules/common/entity/Article.java
+++ b/src/main/java/com/imyeyu/api/modules/common/entity/Article.java
@@ -34,6 +34,8 @@ public class Article extends UUIDEntity implements SettingSupport {
/// GAO 门店日记
GAO_STORE_DIARY,
+ GAO_SYSTEM,
+
/// 反馈
FEEDBACK
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java
index 1028d86..b67202c 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java
@@ -111,6 +111,8 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
CUSTOMER_PACKAGE_PRICE_OVERRIDE("CUSTOMER_PACKAGE:PRICE_OVERRIDE", "客户套餐成交价和权益配额调整", true, true),
+ CUSTOMER_PACKAGE_GIFT_CREATE("CUSTOMER_PACKAGE:GIFT_CREATE", "客户套餐创建赠送", true, true),
+
FUND_RECORD_READ("FUND_RECORD:READ", "资金流水读取", true, true),
FUND_RECEIVE_CREATE("FUND_RECEIVE:CREATE", "线下收款创建", true, true),
diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoSystemSetting.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoSystemSetting.java
new file mode 100644
index 0000000..70c0884
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoSystemSetting.java
@@ -0,0 +1,27 @@
+package com.imyeyu.api.modules.gao.bean;
+
+import com.imyeyu.api.annotation.SettingBizType;
+import com.imyeyu.api.bean.SettingEnum;
+import com.imyeyu.api.modules.common.entity.Setting;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+///
+///
+/// @author 夜雨
+/// @since 2026-09-09 16:11
+@Getter
+@SettingBizType("GAO_SYSTEM")
+@AllArgsConstructor
+public enum GaoSystemSetting implements SettingEnum {
+
+ SERVICE_LICENSE(null, Setting.ValueType.STRING),
+
+ PRIVACY_LICENSE(null, Setting.ValueType.STRING),
+
+ OPEN_SOURCE_LICENSE(null, Setting.ValueType.STRING);
+
+ final String defaultValue;
+
+ final Setting.ValueType valueType;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java
index 0caf16e..07222c5 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java
@@ -1,8 +1,8 @@
package com.imyeyu.api.modules.gao.controller;
+import com.imyeyu.api.bean.PermissionMatchMode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
-import com.imyeyu.api.bean.PermissionMatchMode;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
@@ -17,6 +17,8 @@ import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderUpdateRequest;
+import com.imyeyu.api.modules.user.service.UserService;
+import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
@@ -40,6 +42,7 @@ public class GaoSalesOrderController {
private final GaoSalesOrderService service;
private final GaoOrderPaymentService paymentService;
private final GaoStoreScopeService storeScopeService;
+ private final UserService userService;
@AOPLog
@RequestRateLimit
@@ -56,7 +59,11 @@ public class GaoSalesOrderController {
@RequireGaoPermission(GaoPermissionCode.SALES_ORDER_READ)
@PostMapping("/detail")
public GaoSalesOrder detail(@RequestParam String id) {
- return service.detail(id);
+ GaoSalesOrder result = service.detail(id);
+ if (TimiJava.isNotEmpty(result.getCancelOperatorUserId())) {
+ result.setCancelOperator(userService.get(result.getCancelOperatorUserId()));
+ }
+ return result;
}
@AOPLog
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java
index 2235453..7d9521b 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java
@@ -34,6 +34,9 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 订单项类型
private ItemType itemType;
+ /// true 为赠送权益
+ private Boolean gifted;
+
/// SKU ID
private String skuId;
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java
index d695043..c6faeb0 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java
@@ -4,6 +4,7 @@ import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
+import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
@@ -44,6 +45,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -151,10 +153,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService requestItemList = new ArrayList<>(request.getItemList());
+ normalizePurchaseItems(requestItemList);
verifyPurchaseItems(requestItemList);
boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO,
- com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
- List itemList = resolvePurchaseItems(config.getStoreId(), requestItemList, configItemList, canOverridePriceAndQuantity);
+ GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
+ boolean canCreateGift = permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.CUSTOMER_PACKAGE_GIFT_CREATE.getValue());
+ List itemList = resolvePurchaseItems(config.getStoreId(), requestItemList, configItemList,
+ canOverridePriceAndQuantity, canCreateGift);
itemList.sort(Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort).thenComparing(GaoCustomerPackagePurchaseItem::getSkuId)
.thenComparing(GaoCustomerPackagePurchaseItem::getSkuUnitId));
request.setItemList(itemList);
@@ -246,6 +251,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService itemList) {
+ Map indexMap = new LinkedHashMap<>();
+ for (int i = 0; i < itemList.size(); i++) {
+ GaoCustomerPackagePurchaseItem item = itemList.get(i);
+ if (item == null) {
+ continue;
+ }
+ item.setGifted(Boolean.TRUE.equals(item.getGifted()));
+ if (item.getSkuId() == null || item.getSkuUnitId() == null || item.getTotalQuantity() == null) {
+ continue;
+ }
+ String key = item.getSkuId() + "|" + item.getSkuUnitId() + "|" + item.getGifted();
+ Integer existingIndex = indexMap.putIfAbsent(key, i);
+ if (existingIndex == null) {
+ continue;
+ }
+ GaoCustomerPackagePurchaseItem existingItem = itemList.get(existingIndex);
+ if (existingItem == null || existingItem.getTotalQuantity() == null) {
+ continue;
+ }
+ existingItem.setTotalQuantity(existingItem.getTotalQuantity().add(item.getTotalQuantity()));
+ itemList.set(i, null);
+ }
+ itemList.removeIf(item -> item == null);
+ }
+
private void verifyPurchaseItems(List itemList) {
Set keySet = new HashSet<>();
for (int i = 0; i < itemList.size(); i++) {
@@ -294,14 +327,15 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数");
- String key = item.getSkuId() + "|" + item.getSkuUnitId();
+ item.setGifted(Boolean.TRUE.equals(item.getGifted()));
+ String key = item.getSkuId() + "|" + item.getSkuUnitId() + "|" + item.getGifted();
TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复");
item.setSort(TimiJava.defaultIfNull(item.getSort(), i));
}
}
private List resolvePurchaseItems(String storeId, List requestItemList,
- List configItemList, boolean canOverridePriceAndQuantity) {
+ List configItemList, boolean canOverridePriceAndQuantity, boolean canCreateGift) {
Map> configItemMap = configItemList.stream()
.collect(Collectors.groupingBy(GaoPackageConfigItem::getSkuId));
Map configBaseQuantityMap = new HashMap<>();
@@ -312,23 +346,32 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService requestBaseQuantityMap = new HashMap<>();
List result = new ArrayList<>();
for (GaoCustomerPackagePurchaseItem requestItem : requestItemList) {
+ boolean gifted = Boolean.TRUE.equals(requestItem.getGifted());
List skuConfigItemList = configItemMap.get(requestItem.getSkuId());
- TimiException.requiredTrue(skuConfigItemList != null && !skuConfigItemList.isEmpty(), "购买 SKU 不属于套餐配置");
+ if (gifted) {
+ TimiException.requiredTrue(canCreateGift, "没有客户套餐创建赠送权限");
+ } else {
+ TimiException.requiredTrue(skuConfigItemList != null && !skuConfigItemList.isEmpty(), "购买 SKU 不属于套餐配置");
+ }
GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(storeId, requestItem.getSkuId(), requestItem.getSkuUnitId());
- TimiException.requiredTrue(skuConfigItemList.stream().anyMatch(configItem -> 0 <= configItem.getBaseQuantityPerUnitSnapshot()
- .compareTo(selection.getUnit().getBaseQuantityPerUnit())), "套餐权益单位不能高于配置单位");
+ if (!gifted) {
+ TimiException.requiredTrue(skuConfigItemList.stream().anyMatch(configItem -> 0 <= configItem.getBaseQuantityPerUnitSnapshot()
+ .compareTo(selection.getUnit().getBaseQuantityPerUnit())), "套餐权益单位不能高于配置单位");
+ }
BigDecimal requestBaseQuantity = toBaseQuantity(requestItem.getTotalQuantity(), selection.getUnit().getBaseQuantityPerUnit(), "套餐权益基础数量最多保留三位小数");
- requestBaseQuantityMap.merge(requestItem.getSkuId(), requestBaseQuantity, BigDecimal::add);
+ if (!gifted) {
+ requestBaseQuantityMap.merge(requestItem.getSkuId(), requestBaseQuantity, BigDecimal::add);
+ }
if (requestItem.getTotalQuantity().signum() == 0) {
continue;
}
if (!Boolean.TRUE.equals(selection.getSku().getAllowDecimalQuantity())) {
TimiException.requiredTrue(requestItem.getTotalQuantity().stripTrailingZeros().scale() <= 0, "该 SKU 不允许小数数量");
}
- GaoPackageConfigItem template = resolveItemTemplate(skuConfigItemList, selection.getUnit().getId());
GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem();
item.setSkuId(selection.getSku().getId());
item.setSkuUnitId(selection.getUnit().getId());
+ item.setGifted(gifted);
item.setProductNameSnapshot(selection.getProduct().getName());
item.setProductIdSnapshot(selection.getProduct().getId());
item.setSkuCodeSnapshot(selection.getSku().getCode());
@@ -337,8 +380,14 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService requestItemList = TimiJava.defaultIfNull(request.getItemList(), List.of());
verifyPurchaseItems(requestItemList);
TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应购买请求内容不一致");
- Map existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
+ Map existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId() + "|"
+ + Boolean.TRUE.equals(item.getGifted()), Function.identity()));
for (GaoCustomerPackagePurchaseItem item : requestItemList) {
- GaoSalesOrderItem existingItem = existingItemMap.get(item.getSkuId() + "|" + item.getSkuUnitId());
+ GaoSalesOrderItem existingItem = existingItemMap.get(item.getSkuId() + "|" + item.getSkuUnitId() + "|" + Boolean.TRUE.equals(item.getGifted()));
TimiException.required(existingItem, "requestNo 对应购买请求内容不一致");
TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT
&& existingItem.getQuantity().compareTo(item.getTotalQuantity()) == 0
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 468656c..ed77d79 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
@@ -453,9 +453,11 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService existingItemList = itemMapper.selectBySalesOrderId(existing.getId());
TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应订单内容不一致");
Map existingItemMap = existingItemList.stream()
- .collect(java.util.stream.Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), java.util.function.Function.identity()));
+ .collect(java.util.stream.Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId() + "|" + Boolean.TRUE.equals(item.getGifted()),
+ java.util.function.Function.identity()));
for (GaoSalesOrderItem requestItem : requestItemList) {
- GaoSalesOrderItem existingItem = existingItemMap.get(requestItem.getSkuId() + "|" + requestItem.getSkuUnitId());
+ GaoSalesOrderItem existingItem = existingItemMap.get(requestItem.getSkuId() + "|" + requestItem.getSkuUnitId() + "|"
+ + Boolean.TRUE.equals(requestItem.getGifted()));
TimiException.required(existingItem, "requestNo 对应订单内容不一致");
TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT
+ && Boolean.TRUE.equals(existingItem.getGifted()) == Boolean.TRUE.equals(requestItem.getGifted())
&& existingItem.getQuantity().compareTo(requestItem.getQuantity()) == 0
&& existingItem.getSort().equals(requestItem.getSort())
&& sameText(existingItem.getRemark(), requestItem.getRemark()), "requestNo 对应订单内容不一致");
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java
index 9058943..5f1bf2d 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java
@@ -17,6 +17,9 @@ public class GaoCustomerPackagePurchaseItem {
/// SKU 单位 ID
private String skuUnitId;
+ /// true 表示请求将该明细作为赠送权益创建
+ private Boolean gifted;
+
/// 产品名称快照,由后端生成
private String productNameSnapshot;
diff --git a/src/main/resources/db/migration/timiserver/V75__support_gao_package_creation_gift.sql b/src/main/resources/db/migration/timiserver/V75__support_gao_package_creation_gift.sql
new file mode 100644
index 0000000..e48451c
--- /dev/null
+++ b/src/main/resources/db/migration/timiserver/V75__support_gao_package_creation_gift.sql
@@ -0,0 +1,5 @@
+ALTER TABLE `gao_sales_order_item`
+ ADD COLUMN `gifted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'true 为赠送权益' AFTER `item_type`,
+ DROP KEY `uk_gao_sales_order_item_sku_unit`,
+ ADD UNIQUE KEY `uk_gao_sales_order_item_sku_unit_gifted` (`sales_order_id`, `sku_id`, `sku_unit_id`, `gifted`),
+ ADD KEY `idx_gao_sales_order_item_order_gifted` (`sales_order_id`, `gifted`, `sort`, `created_at`);