OrderSceneTest.java
21.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
package com.onto.engine;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* 「新增订单/新增客户」通用执行端到端测试:验证模型驱动的校验、风控、超卖防护、权限、跨聚合一致性。
*/
@SpringBootTest
@AutoConfigureMockMvc
class OrderSceneTest {
@Autowired
MockMvc mvc;
@Autowired
JdbcTemplate jdbc;
@Autowired
ObjectMapper json;
/** 建一单并返回 orderId(用于 modify/cancel 场景测试)。 */
private String createOrder(String productId, int qty) throws Exception {
String body = ("""
{"master":{"customerId":"C002","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
"details":{"OrderItem":[{"productId":"%s","quantity":%d,"itemPrice":100,"itemCurrency":"CNY"}],
"PaymentTerm":[{"paymentType":"月结","dueDays":30}],
"DeliveryAddressEntity":[{"province":"广东","city":"深圳","district":"南山","streetDetail":"旧地址",
"receiverName":"李四","receiverPhone":"13800002222"}]}}
""").formatted(productId, qty);
String resp = mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(jsonPath("$.success").value(true)).andReturn().getResponse().getContentAsString();
return json.readTree(resp).get("rootId").asText();
}
@Test
void modifyOrderAddress_updatesChildEntityRow() throws Exception {
String oid = createOrder("P003", 1);
String mod = ("""
{"master":{"orderId":"%s"},
"details":{"DeliveryAddressEntity":[{"province":"北京","city":"北京","district":"海淀",
"streetDetail":"新地址999号","receiverName":"李四","receiverPhone":"13900009999"}]}}
""").formatted(oid);
mvc.perform(post("/api/scene/SCENE_MODIFY_ORDER_ADDR/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content(mod))
.andExpect(jsonPath("$.success").value(true));
String street = jdbc.queryForObject(
"SELECT street FROM t_order_address WHERE order_id = ?", String.class, oid);
assertThat(street).isEqualTo("新地址999号");
}
@Test
void cancelOrder_returnsStockViaGenericStockReturn() throws Exception {
String oid = createOrder("P001", 3);
Long afterCreate = jdbc.queryForObject(
"SELECT stock_num FROM t_product_main WHERE product_id = 'P001'", Long.class);
mvc.perform(post("/api/scene/SCENE_CANCEL_ORDER/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content("{\"master\":{\"orderId\":\"" + oid + "\"}}"))
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.stockActions[0].op").value("increment"));
Long afterCancel = jdbc.queryForObject(
"SELECT stock_num FROM t_product_main WHERE product_id = 'P001'", Long.class);
assertThat(afterCancel).isEqualTo(afterCreate + 3);
}
@Test
void createProduct_asAdmin_persists_normalUserDenied() throws Exception {
String body = """
{"master":{"productName":"测试键帽","skuCode":"SKU-KC-999","category":"配件",
"saleCurrency":"CNY","salePrice":99,"stockNum":10,"isOnSale":true}}
""";
String resp = mvc.perform(post("/api/scene/SCENE_CREATE_PRODUCT/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.rootIdField").value("productId")).andReturn().getResponse().getContentAsString();
String pid = json.readTree(resp).get("rootId").asText();
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM t_product_main WHERE product_id = ?", Long.class, pid))
.isEqualTo(1L);
// 普通用户无 product_create 权限 → 拒绝
mvc.perform(post("/api/scene/SCENE_CREATE_PRODUCT/execute").header("X-Principal", "normal_user")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.stage").value("PERMISSION"));
}
@Test
void createProduct_negativePrice_isRejectedByConstraint() throws Exception {
String body = """
{"master":{"productName":"坏价商品","skuCode":"SKU-BAD-1","category":"x",
"saleCurrency":"CNY","salePrice":-5,"stockNum":10,"isOnSale":true}}
""";
mvc.perform(post("/api/scene/SCENE_CREATE_PRODUCT/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.stage").value("VALIDATION"));
}
@Test
void createProduct_duplicateSku_isRejectedByUnique() throws Exception {
// 种子商品 P001 的 skuCode = SKU-NB-001,再建同 sku 应被唯一性约束拦截
String body = """
{"master":{"productName":"重复SKU","skuCode":"SKU-NB-001","category":"x",
"saleCurrency":"CNY","salePrice":10,"stockNum":1,"isOnSale":true}}
""";
mvc.perform(post("/api/scene/SCENE_CREATE_PRODUCT/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.stage").value("VALIDATION"));
}
@Test
void modifyProductPrice_updatesRoot() throws Exception {
mvc.perform(post("/api/scene/SCENE_MODIFY_PRODUCT_PRICE/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"master\":{\"productId\":\"P003\",\"salePrice\":333,\"saleCurrency\":\"CNY\"}}"))
.andExpect(jsonPath("$.success").value(true));
assertThat(jdbc.queryForObject("SELECT sale_price FROM t_product_main WHERE product_id='P003'", Double.class))
.isEqualTo(333.0);
}
@Test
void modifyCustomer_updatesRoot() throws Exception {
mvc.perform(post("/api/scene/SCENE_MODIFY_CUSTOMER/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"master\":{\"customerId\":\"C003\",\"customerLevel\":\"普通\"}}"))
.andExpect(jsonPath("$.success").value(true));
assertThat(jdbc.queryForObject("SELECT cust_level FROM t_customer_main WHERE customer_id='C003'", String.class))
.isEqualTo("普通");
}
@Test
void auditOrder_highAmountPass_updatesOrderAndBumpsAuditorApproveCountAndRemark() throws Exception {
// 金额 2*100=200 (>100) + PASS:订单置 PASS、REMOTE_QUERY 回填审核人名;
// IF(root.totalAmount>100) → REMOTE_SET_FIELD 覆盖员工备注;IF(PASS) → REMOTE_INCREMENT_FIELD 通过数 +1。
String oid = createOrder("P003", 2);
String body = ("""
{"master":{"orderId":"%s","auditorId":"E001","auditResult":"PASS","auditRemark":"资料齐全,通过"}}
""").formatted(oid);
mvc.perform(post("/api/scene/SCENE_AUDIT_ORDER/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.command").value("AuditOrder"));
// 订单侧
assertThat(jdbc.queryForObject("SELECT order_status FROM t_order_main WHERE order_id=?", String.class, oid))
.isEqualTo("PASS");
assertThat(jdbc.queryForObject("SELECT auditor_name FROM t_order_main WHERE order_id=?", String.class, oid))
.isEqualTo("审核员小王"); // REMOTE_QUERY_SET_FIELD 回填
// 审核人(员工)侧:条件回写
assertThat(jdbc.queryForObject("SELECT approve_count FROM t_employee_main WHERE employee_id='E001'", Long.class))
.isEqualTo(1L);
assertThat(jdbc.queryForObject("SELECT remark FROM t_employee_main WHERE employee_id='E001'", String.class))
.isEqualTo("资料齐全,通过");
assertThat(jdbc.queryForObject("SELECT last_audit_time FROM t_employee_main WHERE employee_id='E001'", Object.class))
.isNotNull();
}
@Test
void auditOrder_lowAmountReject_skipsRemarkAndBumpsRejectCount() throws Exception {
// 金额 1*100=100 (不 >100) + REJECT:IF(root.totalAmount>100) 为假 → 不写员工备注;IF(REJECT) → 驳回数 +1。
String oid = createOrder("P001", 1);
String body = ("""
{"master":{"orderId":"%s","auditorId":"E002","auditResult":"REJECT","auditRemark":"信息不全"}}
""").formatted(oid);
mvc.perform(post("/api/scene/SCENE_AUDIT_ORDER/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(jsonPath("$.success").value(true));
assertThat(jdbc.queryForObject("SELECT reject_count FROM t_employee_main WHERE employee_id='E002'", Long.class))
.isEqualTo(1L);
// 金额不足,备注分支未执行,员工备注保持为空
assertThat(jdbc.queryForObject("SELECT remark FROM t_employee_main WHERE employee_id='E002'", String.class))
.isNull();
}
@Test
void createEmployee_initsCountsToZero_thenModify_normalUserDenied() throws Exception {
// CreateEmployee 的 bizSteps 用 SET_FIELD 把两个审核计数初始化为 0
String create = """
{"master":{"employeeName":"新审核员小赵","department":"风控部"}}
""";
String resp = mvc.perform(post("/api/scene/SCENE_CREATE_EMPLOYEE/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content(create))
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.rootIdField").value("employeeId")).andReturn().getResponse().getContentAsString();
String eid = json.readTree(resp).get("rootId").asText();
assertThat(jdbc.queryForObject("SELECT approve_count FROM t_employee_main WHERE employee_id=?", Long.class, eid))
.isEqualTo(0L);
assertThat(jdbc.queryForObject("SELECT reject_count FROM t_employee_main WHERE employee_id=?", Long.class, eid))
.isEqualTo(0L);
// ModifyEmployee:改部门 + 备注
String modify = ("""
{"master":{"employeeId":"%s","department":"合规部","remark":"转岗"}}
""").formatted(eid);
mvc.perform(post("/api/scene/SCENE_MODIFY_EMPLOYEE/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content(modify))
.andExpect(jsonPath("$.success").value(true));
assertThat(jdbc.queryForObject("SELECT dept FROM t_employee_main WHERE employee_id=?", String.class, eid))
.isEqualTo("合规部");
assertThat(jdbc.queryForObject("SELECT remark FROM t_employee_main WHERE employee_id=?", String.class, eid))
.isEqualTo("转岗");
// 普通用户无 employee_create 权限 → 拒绝
mvc.perform(post("/api/scene/SCENE_CREATE_EMPLOYEE/execute").header("X-Principal", "normal_user")
.contentType(MediaType.APPLICATION_JSON).content(create))
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.stage").value("PERMISSION"));
}
@Test
void demoStepTypes_noTemplate_interpretsAllStepTypes() throws Exception {
// 无表单场景:空 payload 直接解释 flowSteps,验证控制流/Saga/EDA/人工各类步骤通用解释成功(不落库)
mvc.perform(post("/api/scene/SCENE_DEMO_STEPTYPES/execute").header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content("{}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true));
}
@Test
void auditScene_schema_rendersAuditorAsEmployeeReferenceDropdown() throws Exception {
// 通用引用下拉:auditorId 带 M1 refAggregate=EmployeeAggregate,合成 Schema 应给出员工选项数据源
mvc.perform(get("/api/meta/scene/SCENE_AUDIT_ORDER"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.template.templateId").value("template_order_audit"))
.andExpect(jsonPath("$.template.rootComponents[0].childComponents[1].fieldBind.optionsSource.aggregateId")
.value("EmployeeAggregate"));
}
@Test
void sensitivePhone_isEncryptedAtRest() {
// M5 storageEncrypt 生效:物理列存密文(enc: 前缀),而非明文手机号
String raw = jdbc.queryForObject("SELECT phone FROM t_customer_main WHERE customer_id = 'C001'", String.class);
assertThat(raw).startsWith("enc:");
assertThat(raw).doesNotContain("13800000001");
}
private static final String VALID_ORDER = """
{"master":{"customerId":"C002","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
"details":{"OrderItem":[{"productId":"P003","quantity":1,"itemPrice":499,"itemCurrency":"CNY"},
{"productId":"P001","quantity":1,"itemPrice":6999,"itemCurrency":"CNY"}],
"PaymentTerm":[{"paymentType":"月结","dueDays":30}],
"DeliveryAddressEntity":[{"province":"广东","city":"深圳","district":"南山","streetDetail":"科技园1号",
"receiverName":"李四","receiverPhone":"13800002222"}]}}
""";
// 普通用户 + 明细金额 8*6999=55992 > 50000(服务端重算),应被 MetaRule REJECT
private static final String OVER_LIMIT_ORDER = """
{"master":{"customerId":"C001","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
"details":{"OrderItem":[{"productId":"P001","quantity":8,"itemPrice":6999,"itemCurrency":"CNY"}],
"PaymentTerm":[{"paymentType":"现金","dueDays":0}],
"DeliveryAddressEntity":[{"province":"广东","city":"深圳","district":"南山","streetDetail":"x",
"receiverName":"张三","receiverPhone":"13800001111"}]}}
""";
private static final String BAD_PHONE_ORDER = """
{"master":{"customerId":"C002","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
"details":{"OrderItem":[{"productId":"P003","quantity":1,"itemPrice":499,"itemCurrency":"CNY"}],
"PaymentTerm":[{"paymentType":"现金","dueDays":0}],
"DeliveryAddressEntity":[{"province":"x","city":"y","district":"z","streetDetail":"w",
"receiverName":"赵六","receiverPhone":"139"}]}}
""";
// P002 库存 8,下单 100 件 → 跨聚合扣减失败 → 整单回滚(防超卖)
private static final String OVERSELL_ORDER = """
{"master":{"customerId":"C002","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
"details":{"OrderItem":[{"productId":"P002","quantity":100,"itemPrice":199,"itemCurrency":"CNY"}],
"PaymentTerm":[{"paymentType":"现金","dueDays":0}],
"DeliveryAddressEntity":[{"province":"x","city":"y","district":"z","streetDetail":"w",
"receiverName":"钱七","receiverPhone":"13800007777"}]}}
""";
// 负数数量:M1 constraints.min=1 应拒绝
private static final String NEGATIVE_QTY_ORDER = """
{"master":{"customerId":"C002","totalCurrency":"CNY","createTime":"2026-07-20T10:00:00"},
"details":{"OrderItem":[{"productId":"P003","quantity":-5,"itemPrice":499,"itemCurrency":"CNY"}],
"PaymentTerm":[{"paymentType":"现金","dueDays":0}],
"DeliveryAddressEntity":[{"province":"x","city":"y","district":"z","streetDetail":"w",
"receiverName":"钱七","receiverPhone":"13800007777"}]}}
""";
private static final String NEW_CUSTOMER = """
{"master":{"customerName":"测试客户","contactPhone":"13900000000","customerLevel":"普通"}}
""";
@Test
void sceneSchema_isComposedFromModel() throws Exception {
mvc.perform(get("/api/meta/scene/SCENE_CREATE_ORDER"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.template.templateType").value("master_detail_form"))
.andExpect(jsonPath("$.commandApi").value("/api/v1/order/create"))
.andExpect(jsonPath("$.template.rootComponents[0].compId").value("card_order_master"));
}
@Test
void createOrder_valid_succeedsWithGenericConsistency() throws Exception {
mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
.contentType(MediaType.APPLICATION_JSON).content(VALID_ORDER))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.rootId").isNotEmpty())
.andExpect(jsonPath("$.rootIdField").value("orderId"))
.andExpect(jsonPath("$.childCount").value(4))
.andExpect(jsonPath("$.stockActions[0].status").value("OK"))
.andExpect(jsonPath("$.stockActions[0].targetCommand").value("StockDeduct"));
}
@Test
void createOrder_overLimitNormalUser_isRejectedByMetaRule() throws Exception {
mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
.contentType(MediaType.APPLICATION_JSON).content(OVER_LIMIT_ORDER))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.stage").value("RULE_REJECT"));
}
@Test
void createOrder_badPhone_failsConstraintValidation() throws Exception {
mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
.contentType(MediaType.APPLICATION_JSON).content(BAD_PHONE_ORDER))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.stage").value("VALIDATION"));
}
@Test
void createOrder_insufficientStock_rollsBackNoOversell() throws Exception {
mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
.contentType(MediaType.APPLICATION_JSON).content(OVERSELL_ORDER))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.stage").value("CONSISTENCY_FAILED"));
}
@Test
void createOrder_negativeQuantity_isRejected() throws Exception {
mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/execute")
.contentType(MediaType.APPLICATION_JSON).content(NEGATIVE_QTY_ORDER))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.stage").value("VALIDATION"));
}
@Test
void precheck_overLimit_flagsReject() throws Exception {
mvc.perform(post("/api/scene/SCENE_CREATE_ORDER/precheck")
.contentType(MediaType.APPLICATION_JSON).content(OVER_LIMIT_ORDER))
.andExpect(status().isOk())
.andExpect(jsonPath("$.reject").value(true))
.andExpect(jsonPath("$.rejectRules[0].ruleId").value("R_ORDER_AMOUNT_LIMIT"));
}
@Test
void createCustomer_requiresAdminPrincipal() throws Exception {
// 管理员:通过
mvc.perform(post("/api/scene/SCENE_CREATE_CUSTOMER/execute")
.header("X-Principal", "backend_admin")
.contentType(MediaType.APPLICATION_JSON).content(NEW_CUSTOMER))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.rootIdField").value("customerId"));
// 普通用户:无权 → PERMISSION
mvc.perform(post("/api/scene/SCENE_CREATE_CUSTOMER/execute")
.header("X-Principal", "normal_user")
.contentType(MediaType.APPLICATION_JSON).content(NEW_CUSTOMER))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.stage").value("PERMISSION"));
}
}