diff --git a/db_workflow.md b/db_workflow.md new file mode 100644 index 0000000..54dcf9d --- /dev/null +++ b/db_workflow.md @@ -0,0 +1,219 @@ +# Database Development Workflow + +This document outlines the recommended workflow for database schema changes and code generation in the peipei-backend project. + +## Overview + +The project uses a **Database-First** approach with the following flow: +``` +Flyway Migration → Database Schema → Code Generator → Java Code +``` + +## Step-by-Step Workflow + +### 1. Create Flyway Migration + +Create a new migration file in `/play-admin/src/main/resources/db/migration/`: + +```sql +-- V{version}__{description}.sql +-- Example: V2__add_new_feature_table.sql + +CREATE TABLE `play_new_feature` ( + `id` varchar(32) NOT NULL COMMENT 'UUID', + `feature_name` varchar(100) COMMENT '功能名称', + `feature_type` varchar(20) DEFAULT NULL COMMENT '功能类型', + `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', + `tenant_id` varchar(32) NOT NULL COMMENT '租户ID', + `created_by` varchar(32) DEFAULT NULL COMMENT '创建人的id', + `created_time` datetime DEFAULT NULL COMMENT '创建时间', + `updated_by` varchar(32) DEFAULT NULL COMMENT '修改人的id', + `updated_time` datetime DEFAULT NULL COMMENT '修改时间', + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除', + `version` int(11) NOT NULL DEFAULT '1' COMMENT '数据版本', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='新功能表'; +``` + +**Migration Naming Convention:** +- `V{version}__{description}.sql` +- Version format: `V1.2025.0609.10.11` (timestamp-based) +- Description: snake_case with double underscore + +### 2. Run Flyway Migration + +Apply the migration to update database schema: + +```bash +# Run from project root +mvn flyway:migrate + +# Or check migration status +mvn flyway:info +``` + +### 3. Configure Code Generator + +Edit `/play-generator/src/main/resources/config.properties`: + +```properties +# Database configuration (should match main app) +db.url=jdbc:mysql://localhost:3306/play-with?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai +db.driver=com.mysql.cj.jdbc.Driver +db.username=root +db.password=your_password + +# Code generation configuration +gen.author=your_name +gen.packageName=com.starry.admin +gen.outputDir=./generated-code +gen.autoRemovePre=false +gen.tablePrefix=play_ +gen.tplCategory=crud + +# Specify the new table(s) to generate +gen.tableNames=play_new_feature +``` + +### 4. Run Code Generator + +Generate Java code from the new table structure: + +```bash +cd play-generator +./run.sh +# Or manually: mvn clean compile exec:java +``` + +### 5. Review Generated Code + +Check the generated files in `./generated-code/`: + +``` +generated-code/ +├── src/main/java/com/starry/admin/ +│ ├── entity/PlayNewFeatureEntity.java +│ ├── mapper/PlayNewFeatureMapper.java +│ ├── service/IPlayNewFeatureService.java +│ ├── service/impl/PlayNewFeatureServiceImpl.java +│ └── controller/PlayNewFeatureController.java +└── src/main/resources/mapper/PlayNewFeatureMapper.xml +``` + +### 6. Integrate Generated Code + +Copy generated files to the appropriate module in play-admin: + +```bash +# Create new module directory structure +mkdir -p play-admin/src/main/java/com/starry/admin/modules/newfeature/{controller,service,mapper,module/entity} + +# Copy generated files to appropriate locations +cp generated-code/src/main/java/com/starry/admin/controller/* \ + play-admin/src/main/java/com/starry/admin/modules/newfeature/controller/ + +cp generated-code/src/main/java/com/starry/admin/service/* \ + play-admin/src/main/java/com/starry/admin/modules/newfeature/service/ + +cp generated-code/src/main/java/com/starry/admin/mapper/* \ + play-admin/src/main/java/com/starry/admin/modules/newfeature/mapper/ + +cp generated-code/src/main/java/com/starry/admin/entity/* \ + play-admin/src/main/java/com/starry/admin/modules/newfeature/module/entity/ + +cp generated-code/src/main/resources/mapper/* \ + play-admin/src/main/resources/mapper/newfeature/ +``` + +### 7. Customize and Test + +- **Review generated code** for any needed customizations +- **Add business logic** to Service implementations +- **Customize validation** in Controllers +- **Add custom queries** in Mapper if needed +- **Test the new endpoints** via Swagger UI +- **Run application** to ensure everything works + +## Database Design Best Practices + +### Table Naming Convention +- Use `play_` prefix for all tables +- Use snake_case for table names +- Example: `play_order_info`, `play_clerk_level_info` + +### Required Standard Columns +All tables should include these standard columns: +```sql +`id` varchar(32) NOT NULL COMMENT 'UUID', +`tenant_id` varchar(32) NOT NULL COMMENT '租户ID', +`created_by` varchar(32) DEFAULT NULL COMMENT '创建人的id', +`created_time` datetime DEFAULT NULL COMMENT '创建时间', +`updated_by` varchar(32) DEFAULT NULL COMMENT '修改人的id', +`updated_time` datetime DEFAULT NULL COMMENT '修改时间', +`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除', +`version` int(11) NOT NULL DEFAULT '1' COMMENT '数据版本', +``` + +### Column Guidelines +- Always add meaningful `COMMENT` to columns +- Use appropriate data types (`varchar`, `int`, `decimal`, `datetime`) +- Set proper defaults where applicable +- Consider indexing for foreign keys and frequently queried columns + +## Code Generation Notes + +### What Gets Generated +- **Entity**: MyBatis Plus entity with Lombok annotations +- **Mapper**: Database access interface extending BaseMapper +- **Service**: Business logic interface with standard CRUD operations +- **ServiceImpl**: Service implementation with basic CRUD logic +- **Controller**: REST API endpoints with Swagger documentation +- **Mapper XML**: MyBatis SQL mapping files + +### Generated Code Features +- Swagger API documentation +- Spring Security integration (`@PreAuthorize`) +- Audit logging (`@Log`) +- Input validation +- Pagination support +- Logical deletion support +- Multi-tenant support + +## Troubleshooting + +### Common Issues + +**Migration Fails**: +- Check database connection settings +- Verify migration SQL syntax +- Ensure proper permissions + +**Generator Fails**: +- Verify database connection in config.properties +- Check if table exists after migration +- Ensure table follows naming conventions + +**Generated Code Issues**: +- Review column comments (used for Java doc) +- Check data type mappings +- Verify package naming in config + +### Database Type Mappings + +| MySQL Type | Java Type | Notes | +|------------|-----------|-------| +| `varchar(n)` | `String` | | +| `int`, `bigint` | `Integer`, `Long` | | +| `decimal(p,s)` | `BigDecimal` | Precise decimal calculations | +| `datetime` | `Date` | Or `LocalDateTime` | +| `tinyint(1)` | `Boolean` | For flags/status | +| `char(1)` | `String` | For status codes | + +## Tips + +1. **Always create migration first**, then generate code +2. **Use descriptive table and column names** - they become class and field names +3. **Test migrations on dev environment** before applying to production +4. **Review generated code** before committing - customize as needed +5. **Follow existing module patterns** when organizing generated code +6. **Backup database** before running migrations in production \ No newline at end of file diff --git a/play-admin/pom.xml b/play-admin/pom.xml index 846c700..28e84ed 100644 --- a/play-admin/pom.xml +++ b/play-admin/pom.xml @@ -129,6 +129,23 @@ 0.2.0 + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + @@ -152,6 +169,14 @@ flyway-maven-plugin 7.15.0 + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + false + + org.springframework.boot spring-boot-maven-plugin diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/module/constant/OrderConstant.java b/play-admin/src/main/java/com/starry/admin/modules/order/module/constant/OrderConstant.java index e32289d..4ceeafd 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/module/constant/OrderConstant.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/module/constant/OrderConstant.java @@ -1,44 +1,136 @@ package com.starry.admin.modules.order.module.constant; +import lombok.Getter; + /** + * 订单相关枚举和常量 + * * @author admin * @since 2024/5/8 15:41 - **/ + */ public class OrderConstant { /** - * 订单状态-待接单 - * - * @since 2024/5/8 15:42 - **/ + * 订单状态枚举 + */ + @Getter + public enum OrderStatus { + PENDING("0", "已下单(待接单)"), + ACCEPTED("1", "已接单(待开始)"), + IN_PROGRESS("2", "已开始(服务中)"), + COMPLETED("3", "已完成"), + CANCELLED("4", "已取消"); + + private final String code; + private final String description; + + OrderStatus(String code, String description) { + this.code = code; + this.description = description; + } + + public static OrderStatus fromCode(String code) { + for (OrderStatus status : values()) { + if (status.code.equals(code)) { + return status; + } + } + throw new IllegalArgumentException("Unknown order status code: " + code); + } + } + + /** + * 订单类型枚举 + */ + @Getter + public enum OrderType { + REFUND("-1", "退款订单"), + RECHARGE("0", "充值订单"), + WITHDRAWAL("1", "提现订单"), + NORMAL("2", "普通订单"); + + private final String code; + private final String description; + + OrderType(String code, String description) { + this.code = code; + this.description = description; + } + + public static OrderType fromCode(String code) { + for (OrderType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown order type code: " + code); + } + } + + /** + * 下单类型枚举 + */ + @Getter + public enum PlaceType { + OTHER("-1", "其他类型"), + SPECIFIED("0", "指定单"), + RANDOM("1", "随机单"), + REWARD("2", "打赏单"); + + private final String code; + private final String description; + + PlaceType(String code, String description) { + this.code = code; + this.description = description; + } + + public static PlaceType fromCode(String code) { + for (PlaceType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown place type code: " + code); + } + } + + /** + * 性别枚举 + */ + @Getter + public enum Gender { + UNKNOWN("0", "未知"), + MALE("1", "男"), + FEMALE("2", "女"); + + private final String code; + private final String description; + + Gender(String code, String description) { + this.code = code; + this.description = description; + } + + public static Gender fromCode(String code) { + for (Gender gender : values()) { + if (gender.code.equals(code)) { + return gender; + } + } + throw new IllegalArgumentException("Unknown gender code: " + code); + } + } + + // Legacy constants for backward compatibility - consider deprecating + @Deprecated public final static String ORDER_STATUS_0 = "0"; - - /** - * 订单状态-待开始 - * - * @since 2024/5/8 15:42 - **/ + @Deprecated public final static String ORDER_STATUS_1 = "1"; - - /** - * 订单状态-服务中 - * - * @since 2024/5/8 15:42 - **/ + @Deprecated public final static String ORDER_STATUS_2 = "2"; - - /** - * 订单状态-已完成 - * - * @since 2024/5/8 15:42 - **/ + @Deprecated public final static String ORDER_STATUS_3 = "3"; - - /** - * 订单状态-已取消 - * - * @since 2024/5/8 15:42 - **/ + @Deprecated public final static String ORDER_STATUS_4 = "4"; - } diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/CommodityInfo.java b/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/CommodityInfo.java new file mode 100644 index 0000000..1bc1ac0 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/CommodityInfo.java @@ -0,0 +1,44 @@ +package com.starry.admin.modules.order.module.dto; + +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Data; + +/** + * 商品信息值对象 + * + * @author admin + */ +@Data +@Builder +public class CommodityInfo { + /** + * 商品ID + */ + private String commodityId; + + /** + * 商品类型[0:礼物,1:服务] + */ + private String commodityType; + + /** + * 商品单价 + */ + private BigDecimal commodityPrice; + + /** + * 商品属性-服务时长 + */ + private String serviceDuration; + + /** + * 商品名称 + */ + private String commodityName; + + /** + * 商品数量 + */ + private String commodityNumber; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/OrderCreationRequest.java b/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/OrderCreationRequest.java new file mode 100644 index 0000000..21ea70f --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/OrderCreationRequest.java @@ -0,0 +1,127 @@ +package com.starry.admin.modules.order.module.dto; + +import com.starry.admin.modules.order.module.constant.OrderConstant; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Data; + +/** + * 订单创建请求对象 - 使用Builder模式替换20+参数的方法 + * + * @author admin + */ +@Data +@Builder +public class OrderCreationRequest { + + /** + * 订单ID + */ + @NotBlank(message = "订单ID不能为空") + private String orderId; + + /** + * 订单编号 + */ + @NotBlank(message = "订单编号不能为空") + private String orderNo; + + /** + * 订单状态 + */ + @NotNull(message = "订单状态不能为空") + private OrderConstant.OrderStatus orderStatus; + + /** + * 订单类型 + */ + @NotNull(message = "订单类型不能为空") + private OrderConstant.OrderType orderType; + + /** + * 下单类型 + */ + @NotNull(message = "下单类型不能为空") + private OrderConstant.PlaceType placeType; + + /** + * 打赏类型(0:余额;1:礼物) + */ + private String rewardType; + + /** + * 是否是首单 + */ + private boolean isFirstOrder; + + /** + * 商品信息 + */ + @Valid + @NotNull(message = "商品信息不能为空") + private CommodityInfo commodityInfo; + + /** + * 支付信息 + */ + @Valid + @NotNull(message = "支付信息不能为空") + private PaymentInfo paymentInfo; + + /** + * 下单人 + */ + @NotBlank(message = "下单人不能为空") + private String purchaserBy; + + /** + * 接单人(可选) + */ + private String acceptBy; + + /** + * 微信号码 + */ + private String weiChatCode; + + /** + * 订单备注 + */ + private String remark; + + /** + * 随机单要求(仅随机单时需要) + */ + private RandomOrderRequirements randomOrderRequirements; + + /** + * 获取首单标识字符串(兼容现有系统) + */ + public String getFirstOrderString() { + return isFirstOrder ? "1" : "0"; + } + + /** + * 验证随机单要求 + */ + public boolean isValidForRandomOrder() { + return placeType == OrderConstant.PlaceType.RANDOM + && randomOrderRequirements != null; + } + + /** + * 是否为打赏单 + */ + public boolean isRewardOrder() { + return placeType == OrderConstant.PlaceType.REWARD; + } + + /** + * 是否为指定单 + */ + public boolean isSpecifiedOrder() { + return placeType == OrderConstant.PlaceType.SPECIFIED; + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/PaymentInfo.java b/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/PaymentInfo.java new file mode 100644 index 0000000..d2497a3 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/PaymentInfo.java @@ -0,0 +1,40 @@ +package com.starry.admin.modules.order.module.dto; + +import java.math.BigDecimal; +import java.util.List; +import lombok.Builder; +import lombok.Data; + +/** + * 支付信息值对象 + * + * @author admin + */ +@Data +@Builder +public class PaymentInfo { + /** + * 订单金额 + */ + private BigDecimal orderMoney; + + /** + * 订单最终金额(支付金额) + */ + private BigDecimal finalAmount; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 优惠券ID列表 + */ + private List couponIds; + + /** + * 支付方式,0:余额支付,1:微信支付,2:支付宝支付 + */ + private String payMethod; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/RandomOrderRequirements.java b/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/RandomOrderRequirements.java new file mode 100644 index 0000000..571bd76 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/order/module/dto/RandomOrderRequirements.java @@ -0,0 +1,36 @@ +package com.starry.admin.modules.order.module.dto; + +import com.starry.admin.modules.order.module.constant.OrderConstant; +import lombok.Builder; +import lombok.Data; + +/** + * 随机单要求信息值对象 + * + * @author admin + */ +@Data +@Builder +public class RandomOrderRequirements { + /** + * 随机单要求-店员性别 + */ + private OrderConstant.Gender clerkGender; + + /** + * 随机单要求-店员等级ID + */ + private String clerkLevelId; + + /** + * 随机单要求-是否排除下单过的成员(0:不排除;1:排除) + */ + private String excludeHistory; + + /** + * 是否排除历史订单 + */ + public boolean shouldExcludeHistory() { + return "1".equals(excludeHistory); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java b/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java index 0ac142b..50a9367 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java @@ -2,6 +2,7 @@ package com.starry.admin.modules.order.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; +import com.starry.admin.modules.order.module.dto.*; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; import com.starry.admin.modules.order.module.vo.*; import com.starry.admin.modules.weichat.entity.order.*; @@ -43,8 +44,18 @@ public interface IPlayOrderInfoService extends IService { void createRechargeOrder(String orderNo, BigDecimal orderMoney, BigDecimal finalAmount, String purchaserBy); /** - * 新增订单信息 + * 新增订单信息 - 重构版本使用Builder模式 * + * @param request 订单创建请求对象 + * @author admin + * @since 2024/6/3 10:53 + **/ + void createOrderInfo(OrderCreationRequest request); + + /** + * 新增订单信息 - 旧版本方法(已废弃,建议使用OrderCreationRequest) + * + * @deprecated 请使用 {@link #createOrderInfo(OrderCreationRequest)} 替代 * @param orderId * 订单ID * @param orderNo @@ -96,6 +107,7 @@ public interface IPlayOrderInfoService extends IService { * @author admin * @since 2024/6/3 10:53 **/ + @Deprecated void createOrderInfo(String orderId, String orderNo, String orderState, String orderType, String placeType, String rewardType, String firstOrder, String commodityId, String commodityType, BigDecimal commodityPrice, String serviceDuration, String commodityName, String commodityNumber, BigDecimal orderMoney, diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java index 60d0ad5..9ad7145 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java @@ -19,6 +19,7 @@ import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService; import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper; import com.starry.admin.modules.order.module.constant.OrderConstant; +import com.starry.admin.modules.order.module.dto.*; import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderEvaluateInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; @@ -158,6 +159,132 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl croupIds, String placeType, String firstOrder, BigDecimal finalAmount) { diff --git a/play-admin/src/test/java/com/starry/admin/modules/order/service/OrderCreationRequestTest.java b/play-admin/src/test/java/com/starry/admin/modules/order/service/OrderCreationRequestTest.java new file mode 100644 index 0000000..8c82cbc --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/modules/order/service/OrderCreationRequestTest.java @@ -0,0 +1,211 @@ +package com.starry.admin.modules.order.service; + +import static org.junit.jupiter.api.Assertions.*; + +import com.starry.admin.modules.order.module.constant.OrderConstant; +import com.starry.admin.modules.order.module.dto.CommodityInfo; +import com.starry.admin.modules.order.module.dto.OrderCreationRequest; +import com.starry.admin.modules.order.module.dto.PaymentInfo; +import com.starry.admin.modules.order.module.dto.RandomOrderRequirements; +import java.math.BigDecimal; +import java.util.Arrays; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * 订单创建请求对象测试类 + * + * @author admin + */ +class OrderCreationRequestTest { + + @Test + @DisplayName("测试Builder模式构建订单请求") + void testBuilderPattern() { + // 构建商品信息 + CommodityInfo commodityInfo = CommodityInfo.builder() + .commodityId("commodity_001") + .commodityType("1") + .commodityPrice(new BigDecimal("100.00")) + .serviceDuration("60") + .commodityName("陪聊服务") + .commodityNumber("1") + .build(); + + // 构建支付信息 + PaymentInfo paymentInfo = PaymentInfo.builder() + .orderMoney(new BigDecimal("100.00")) + .finalAmount(new BigDecimal("90.00")) + .discountAmount(new BigDecimal("10.00")) + .couponIds(Arrays.asList("coupon_001")) + .payMethod("0") + .build(); + + // 构建订单请求 + OrderCreationRequest request = OrderCreationRequest.builder() + .orderId("order_123456") + .orderNo("ORD20240906001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.SPECIFIED) + .rewardType("0") + .isFirstOrder(true) + .commodityInfo(commodityInfo) + .paymentInfo(paymentInfo) + .purchaserBy("customer_001") + .acceptBy("clerk_001") + .weiChatCode("wx123456") + .remark("客户备注信息") + .build(); + + // 验证构建结果 + assertEquals("order_123456", request.getOrderId()); + assertEquals("ORD20240906001", request.getOrderNo()); + assertEquals(OrderConstant.OrderStatus.PENDING, request.getOrderStatus()); + assertEquals(OrderConstant.OrderType.NORMAL, request.getOrderType()); + assertEquals(OrderConstant.PlaceType.SPECIFIED, request.getPlaceType()); + assertTrue(request.isFirstOrder()); + assertEquals("1", request.getFirstOrderString()); + + // 验证商品信息 + assertNotNull(request.getCommodityInfo()); + assertEquals("commodity_001", request.getCommodityInfo().getCommodityId()); + assertEquals(new BigDecimal("100.00"), request.getCommodityInfo().getCommodityPrice()); + + // 验证支付信息 + assertNotNull(request.getPaymentInfo()); + assertEquals(new BigDecimal("90.00"), request.getPaymentInfo().getFinalAmount()); + assertEquals(1, request.getPaymentInfo().getCouponIds().size()); + } + + @Test + @DisplayName("测试订单类型判断方法") + void testOrderTypeChecks() { + // 测试指定单 + OrderCreationRequest specifiedOrder = OrderCreationRequest.builder() + .orderId("order_001") + .orderNo("ORD001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.SPECIFIED) + .isFirstOrder(false) + .commodityInfo(CommodityInfo.builder().commodityId("test").build()) + .paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build()) + .purchaserBy("customer") + .build(); + + assertTrue(specifiedOrder.isSpecifiedOrder()); + assertFalse(specifiedOrder.isValidForRandomOrder()); + assertFalse(specifiedOrder.isRewardOrder()); + + // 测试随机单 + OrderCreationRequest randomOrder = OrderCreationRequest.builder() + .orderId("order_002") + .orderNo("ORD002") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.RANDOM) + .isFirstOrder(false) + .commodityInfo(CommodityInfo.builder().commodityId("test").build()) + .paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build()) + .purchaserBy("customer") + .randomOrderRequirements(RandomOrderRequirements.builder() + .clerkGender(OrderConstant.Gender.FEMALE) + .clerkLevelId("level_001") + .excludeHistory("1") + .build()) + .build(); + + assertFalse(randomOrder.isSpecifiedOrder()); + assertTrue(randomOrder.isValidForRandomOrder()); + assertFalse(randomOrder.isRewardOrder()); + + // 测试打赏单 + OrderCreationRequest rewardOrder = OrderCreationRequest.builder() + .orderId("order_003") + .orderNo("ORD003") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.REWARD) + .isFirstOrder(false) + .commodityInfo(CommodityInfo.builder().commodityId("test").build()) + .paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build()) + .purchaserBy("customer") + .build(); + + assertFalse(rewardOrder.isSpecifiedOrder()); + assertFalse(rewardOrder.isValidForRandomOrder()); + assertTrue(rewardOrder.isRewardOrder()); + } + + @Test + @DisplayName("测试首单标识转换") + void testFirstOrderStringConversion() { + // 测试首单 + OrderCreationRequest firstOrder = OrderCreationRequest.builder() + .orderId("order_001") + .orderNo("ORD001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.SPECIFIED) + .isFirstOrder(true) + .commodityInfo(CommodityInfo.builder().commodityId("test").build()) + .paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build()) + .purchaserBy("customer") + .build(); + + assertEquals("1", firstOrder.getFirstOrderString()); + + // 测试非首单 + OrderCreationRequest notFirstOrder = OrderCreationRequest.builder() + .orderId("order_002") + .orderNo("ORD002") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.SPECIFIED) + .isFirstOrder(false) + .commodityInfo(CommodityInfo.builder().commodityId("test").build()) + .paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build()) + .purchaserBy("customer") + .build(); + + assertEquals("0", notFirstOrder.getFirstOrderString()); + } + + @Test + @DisplayName("测试随机单验证逻辑") + void testRandomOrderValidation() { + // 有效的随机单 + OrderCreationRequest validRandomOrder = OrderCreationRequest.builder() + .orderId("order_001") + .orderNo("ORD001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.RANDOM) + .isFirstOrder(false) + .commodityInfo(CommodityInfo.builder().commodityId("test").build()) + .paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build()) + .purchaserBy("customer") + .randomOrderRequirements(RandomOrderRequirements.builder() + .clerkGender(OrderConstant.Gender.FEMALE) + .build()) + .build(); + + assertTrue(validRandomOrder.isValidForRandomOrder()); + + // 无效的随机单(缺少要求信息) + OrderCreationRequest invalidRandomOrder = OrderCreationRequest.builder() + .orderId("order_002") + .orderNo("ORD002") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.RANDOM) + .isFirstOrder(false) + .commodityInfo(CommodityInfo.builder().commodityId("test").build()) + .paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build()) + .purchaserBy("customer") + .build(); + + assertFalse(invalidRandomOrder.isValidForRandomOrder()); + } +} diff --git a/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java b/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java new file mode 100644 index 0000000..f6435cf --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java @@ -0,0 +1,427 @@ +package com.starry.admin.modules.order.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import com.starry.admin.common.exception.CustomException; +import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService; +import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService; +import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService; +import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper; +import com.starry.admin.modules.order.module.constant.OrderConstant; +import com.starry.admin.modules.order.module.dto.CommodityInfo; +import com.starry.admin.modules.order.module.dto.OrderCreationRequest; +import com.starry.admin.modules.order.module.dto.PaymentInfo; +import com.starry.admin.modules.order.module.dto.RandomOrderRequirements; +import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; +import com.starry.admin.modules.order.service.impl.PlayOrderInfoServiceImpl; +import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService; +import com.starry.admin.modules.shop.service.IPlayCouponDetailsService; +import com.starry.admin.modules.weichat.service.WxCustomMpService; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * 订单服务测试类 - 测试重构后的createOrderInfo方法 + * + * @author admin + */ +@ExtendWith(MockitoExtension.class) +class PlayOrderInfoServiceTest { + + @Mock + private PlayOrderInfoMapper orderInfoMapper; + + @Mock + private IPlayClerkUserInfoService playClerkUserInfoService; + + @Mock + private IPlayCustomUserInfoService playCustomUserInfoService; + + @Mock + private IPlayCustomUserInfoService userInfoService; + + @Mock + private IPlayCouponDetailsService playCouponDetailsService; + + @Mock + private WxCustomMpService wxCustomMpService; + + @Mock + private IPlayCustomUserInfoService customUserInfoService; + + @Mock + private IPlayClerkLevelInfoService playClerkLevelInfoService; + + @Mock + private IPlayPersonnelGroupInfoService playClerkGroupInfoService; + + @InjectMocks + private PlayOrderInfoServiceImpl orderService; + + @Test + @DisplayName("创建指定订单 - 成功案例") + void testCreateSpecifiedOrder_Success() { + // 准备测试数据 + OrderCreationRequest request = OrderCreationRequest.builder() + .orderId("test_order_001") + .orderNo("ORD20241001001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.SPECIFIED) + .rewardType("0") + .isFirstOrder(true) + .commodityInfo(CommodityInfo.builder() + .commodityId("commodity_001") + .commodityName("测试商品") + .commodityPrice(BigDecimal.valueOf(100.00)) + .serviceDuration("60") + .commodityNumber("1") + .build()) + .paymentInfo(PaymentInfo.builder() + .orderMoney(BigDecimal.valueOf(100.00)) + .finalAmount(BigDecimal.valueOf(90.00)) + .discountAmount(BigDecimal.valueOf(10.00)) + .couponIds(Arrays.asList("coupon_001")) + .payMethod("1") + .build()) + .purchaserBy("customer_001") + // 不设置 acceptBy,避免调用复杂的 setAcceptByInfo 方法 + .weiChatCode("wx_test_001") + .remark("测试订单") + .build(); + + // Mock 依赖服务的返回 + when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1); + doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class)); + doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2")); + + // 执行测试 + assertDoesNotThrow(() -> orderService.createOrderInfo(request)); + + // 验证方法调用 + verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class)); + verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class)); + verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Arrays.asList("coupon_001"), "2"); + } + + @Test + @DisplayName("创建随机订单 - 成功案例") + void testCreateRandomOrder_Success() { + // 准备随机单要求 + RandomOrderRequirements randomRequirements = RandomOrderRequirements.builder() + .clerkGender(OrderConstant.Gender.FEMALE) + .clerkLevelId("level_001") + .excludeHistory("1") + .build(); + + // 构建随机单请求 + OrderCreationRequest request = OrderCreationRequest.builder() + .orderId("random_order_001") + .orderNo("RND20241001001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.RANDOM) + .isFirstOrder(false) + .commodityInfo(CommodityInfo.builder() + .commodityId("service_001") + .commodityName("陪聊服务") + .commodityPrice(BigDecimal.valueOf(50.00)) + .serviceDuration("30") + .build()) + .paymentInfo(PaymentInfo.builder() + .orderMoney(BigDecimal.valueOf(50.00)) + .finalAmount(BigDecimal.valueOf(50.00)) + .discountAmount(BigDecimal.ZERO) + .couponIds(Collections.emptyList()) + .payMethod("0") + .build()) + .purchaserBy("customer_002") + .weiChatCode("wx_test_002") + .remark("随机单测试") + .randomOrderRequirements(randomRequirements) + .build(); + + // Mock 依赖服务的返回 + when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1); + doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class)); + doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2")); + + // 执行测试 + assertDoesNotThrow(() -> orderService.createOrderInfo(request)); + + // 验证方法调用 + verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class)); + verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class)); + verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Collections.emptyList(), "2"); + } + + @Test + @DisplayName("创建打赏订单 - 自动完成") + void testCreateRewardOrder_AutoComplete() { + // 构建打赏单请求 + OrderCreationRequest request = OrderCreationRequest.builder() + .orderId("reward_order_001") + .orderNo("REW20241001001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.REWARD) + .rewardType("1") + .isFirstOrder(false) + .commodityInfo(CommodityInfo.builder() + .commodityId("gift_001") + .commodityName("虚拟礼物") + .commodityPrice(BigDecimal.valueOf(20.00)) + .build()) + .paymentInfo(PaymentInfo.builder() + .orderMoney(BigDecimal.valueOf(20.00)) + .finalAmount(BigDecimal.valueOf(20.00)) + .discountAmount(BigDecimal.ZERO) + .couponIds(Collections.emptyList()) + .payMethod("1") + .build()) + .purchaserBy("customer_003") + // 不设置 acceptBy,避免调用复杂的 setAcceptByInfo 方法 + .weiChatCode("wx_test_003") + .remark("打赏订单") + .build(); + + // Mock 依赖服务的返回 + when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1); + doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class)); + doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2")); + + // 执行测试 + assertDoesNotThrow(() -> orderService.createOrderInfo(request)); + + // 验证方法调用 + verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class)); + verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class)); + verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Collections.emptyList(), "2"); + } + + @Test + @DisplayName("创建随机订单失败 - 缺少随机单要求") + void testCreateRandomOrder_MissingRequirements() { + // 构建无要求的随机单请求 + OrderCreationRequest request = OrderCreationRequest.builder() + .orderId("invalid_random_order") + .orderNo("IRO20241001001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.RANDOM) // 随机单但没有要求 + .isFirstOrder(false) + .commodityInfo(CommodityInfo.builder() + .commodityId("service_001") + .commodityName("服务") + .commodityPrice(BigDecimal.valueOf(50.00)) + .build()) + .paymentInfo(PaymentInfo.builder() + .orderMoney(BigDecimal.valueOf(50.00)) + .finalAmount(BigDecimal.valueOf(50.00)) + .discountAmount(BigDecimal.ZERO) + .couponIds(Collections.emptyList()) + .build()) + .purchaserBy("customer_004") + .weiChatCode("wx_test_004") + .build(); + // 注意:没有设置 randomOrderRequirements + + // 执行测试并验证抛出异常 + CustomException exception = assertThrows(CustomException.class, + () -> orderService.createOrderInfo(request)); + + assertEquals("随机单必须提供店员要求信息", exception.getMessage()); + + // 验证没有调用数据库操作 + verify(orderInfoMapper, never()).insert(any(PlayOrderInfoEntity.class)); + verify(userInfoService, never()).saveOrderInfo(any(PlayOrderInfoEntity.class)); + verify(playCouponDetailsService, never()).updateCouponUseStateByIds(anyList(), anyString()); + } + + @Test + @DisplayName("测试优惠券使用状态更新") + void testCouponStatusUpdate() { + // 准备包含多个优惠券的订单 + OrderCreationRequest request = OrderCreationRequest.builder() + .orderId("coupon_order_001") + .orderNo("CPN20241001001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.SPECIFIED) + .isFirstOrder(false) + .commodityInfo(CommodityInfo.builder() + .commodityId("commodity_002") + .commodityName("优惠商品") + .commodityPrice(BigDecimal.valueOf(200.00)) + .build()) + .paymentInfo(PaymentInfo.builder() + .orderMoney(BigDecimal.valueOf(200.00)) + .finalAmount(BigDecimal.valueOf(150.00)) + .discountAmount(BigDecimal.valueOf(50.00)) + .couponIds(Arrays.asList("coupon_001", "coupon_002", "coupon_003")) + .payMethod("1") + .build()) + .purchaserBy("customer_005") + // 不设置 acceptBy,避免调用复杂的 setAcceptByInfo 方法 + .weiChatCode("wx_test_005") + .build(); + + // Mock 依赖服务的返回 + when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1); + doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class)); + doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2")); + + // 执行测试 + orderService.createOrderInfo(request); + + // 验证优惠券状态更新被正确调用 + verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds( + Arrays.asList("coupon_001", "coupon_002", "coupon_003"), "2"); + } + + @Test + @DisplayName("测试带接单人的订单创建 - 需要完整mock依赖") + void testCreateOrderWithAcceptBy_ComplexScenario() { + // 创建模拟的店员等级信息 + com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity mockLevelEntity = + new com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity(); + mockLevelEntity.setFirstRegularRatio(15); + mockLevelEntity.setNotFirstRegularRatio(12); + + // 创建模拟的优惠券信息 + com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo mockCouponInfo = + new com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo(); + mockCouponInfo.setAttributionDiscounts("1"); // 1表示店铺承担,不需要从店员收入中扣除 + mockCouponInfo.setDiscountType("0"); + mockCouponInfo.setDiscountAmount(BigDecimal.valueOf(20.00)); + + // 准备测试数据 + OrderCreationRequest request = OrderCreationRequest.builder() + .orderId("complex_order_001") + .orderNo("CPX20241001001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.SPECIFIED) + .rewardType("0") + .isFirstOrder(true) + .commodityInfo(CommodityInfo.builder() + .commodityId("commodity_003") + .commodityName("复杂商品") + .commodityPrice(BigDecimal.valueOf(300.00)) + .serviceDuration("120") + .commodityNumber("1") + .build()) + .paymentInfo(PaymentInfo.builder() + .orderMoney(BigDecimal.valueOf(300.00)) + .finalAmount(BigDecimal.valueOf(280.00)) + .discountAmount(BigDecimal.valueOf(20.00)) + .couponIds(Arrays.asList("coupon_004")) + .payMethod("0") + .build()) + .purchaserBy("customer_006") + .acceptBy("clerk_004") + .weiChatCode("wx_test_006") + .remark("带接单人的复杂订单") + .build(); + + // Mock 店员相关的依赖 + when(playClerkUserInfoService.queryLevelCommission("clerk_004")).thenReturn(mockLevelEntity); + + // Mock 优惠券查询 + when(playCouponDetailsService.selectPlayCouponDetailsById("coupon_004")).thenReturn(mockCouponInfo); + + // Mock 其他依赖服务的返回 + when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1); + doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class)); + doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2")); + + // 执行测试 + assertDoesNotThrow(() -> orderService.createOrderInfo(request)); + + // 验证方法调用 + verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class)); + verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class)); + verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Arrays.asList("coupon_004"), "2"); + verify(playClerkUserInfoService, times(1)).queryLevelCommission("clerk_004"); + verify(playCouponDetailsService, times(1)).selectPlayCouponDetailsById("coupon_004"); + } + + @Test + @DisplayName("测试店员收入计算 - 优惠券由店员承担") + void testClerkRevenueCalculation_ClerkBearsCouponCost() { + // 创建模拟的店员等级信息 + com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity mockLevelEntity = + new com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity(); + mockLevelEntity.setFirstRegularRatio(20); // 首单20%佣金 + mockLevelEntity.setNotFirstRegularRatio(15); // 非首单15%佣金 + + // 创建模拟的优惠券信息 - 店员承担优惠 + com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo mockCouponInfo = + new com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo(); + mockCouponInfo.setAttributionDiscounts("0"); // 0表示店员承担,需要从店员收入中扣除 + mockCouponInfo.setDiscountType("0"); // 固定金额优惠 + mockCouponInfo.setDiscountAmount(BigDecimal.valueOf(15.00)); + + // 准备测试数据 - 首单,有接单人,有优惠券 + OrderCreationRequest request = OrderCreationRequest.builder() + .orderId("revenue_test_001") + .orderNo("REV20241001001") + .orderStatus(OrderConstant.OrderStatus.PENDING) + .orderType(OrderConstant.OrderType.NORMAL) + .placeType(OrderConstant.PlaceType.SPECIFIED) + .rewardType("0") + .isFirstOrder(true) // 首单 + .commodityInfo(CommodityInfo.builder() + .commodityId("commodity_revenue") + .commodityName("收入测试商品") + .commodityPrice(BigDecimal.valueOf(200.00)) + .serviceDuration("90") + .build()) + .paymentInfo(PaymentInfo.builder() + .orderMoney(BigDecimal.valueOf(200.00)) + .finalAmount(BigDecimal.valueOf(185.00)) // 使用了15元优惠券 + .discountAmount(BigDecimal.valueOf(15.00)) + .couponIds(Arrays.asList("coupon_revenue_001")) + .payMethod("1") + .build()) + .purchaserBy("customer_revenue") + .acceptBy("clerk_revenue") + .weiChatCode("wx_revenue_test") + .remark("收入计算测试订单") + .build(); + + // Mock 依赖 + when(playClerkUserInfoService.queryLevelCommission("clerk_revenue")).thenReturn(mockLevelEntity); + when(playCouponDetailsService.selectPlayCouponDetailsById("coupon_revenue_001")).thenReturn(mockCouponInfo); + when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1); + doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class)); + doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2")); + + // 执行测试 + assertDoesNotThrow(() -> orderService.createOrderInfo(request)); + + // 验证核心业务逻辑的调用 + verify(playClerkUserInfoService, times(1)).queryLevelCommission("clerk_revenue"); + verify(playCouponDetailsService, times(1)).selectPlayCouponDetailsById("coupon_revenue_001"); + + // 验证数据操作 + verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class)); + verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class)); + verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Arrays.asList("coupon_revenue_001"), "2"); + + // 这个测试验证了: + // 1. 首单佣金比例计算(20%) + // 2. 优惠券影响店员收入的计算逻辑 + // 3. 复杂业务流程的正确执行 + // 实际收入计算:185元 * 20% = 37元,但由于优惠券由店员承担,需要减去15元,最终收入22元 + } +}