commit fff93846e6a66a0be987244dc2a95ad998babbeb Author: zhoukaihong@idcos.com Date: Tue Feb 18 23:35:50 2025 +0800 代码 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..68623a0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.jzx + tuling-flight-booking + 0.0.1-SNAPSHOT + tuling-flight-booking + stuling-flight-booking + + + 17 + 1.0.0-M3 + + + + + org.springframework.boot + spring-boot-starter-web + + + + + + + + + + + + org.springframework.ai + spring-ai-ollama-spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/src/main/java/com/xs/ai/SpringAiDemoApplication.java b/src/main/java/com/xs/ai/SpringAiDemoApplication.java new file mode 100644 index 0000000..6b52452 --- /dev/null +++ b/src/main/java/com/xs/ai/SpringAiDemoApplication.java @@ -0,0 +1,55 @@ +package com.xs.ai; + +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.memory.InMemoryChatMemory; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.reader.TextReader; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.Resource; + +/** + * @author xushu + * @version 1.0.0 + * @description + */ +@SpringBootApplication +public class SpringAiDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringAiDemoApplication.class, args); + } + + // In the real world, ingesting documents would often happen separately, on a CI + // server or similar. + @Bean + CommandLineRunner ingestTermOfServiceToVectorStore(EmbeddingModel embeddingModel, VectorStore vectorStore, + @Value("classpath:rag/terms-of-service.txt") Resource termsOfServiceDocs) { + + return args -> { + // Ingest the document into the vector store + vectorStore.write( // 3.写入 + new TokenTextSplitter().transform( // 2.转换 + new TextReader(termsOfServiceDocs).read()) // 1.读取 + ); + + }; + } + + @Bean + public ChatMemory chatMemory() { + return new InMemoryChatMemory(); + } + + @Bean + public VectorStore vectorStore(EmbeddingModel embeddingModel) { + return new SimpleVectorStore(embeddingModel); + } + +} diff --git a/src/main/java/com/xs/ai/controller/BookingController.java b/src/main/java/com/xs/ai/controller/BookingController.java new file mode 100644 index 0000000..102e9e8 --- /dev/null +++ b/src/main/java/com/xs/ai/controller/BookingController.java @@ -0,0 +1,30 @@ +package com.xs.ai.controller; + + +import com.xs.ai.services.BookingTools; +import com.xs.ai.services.BookingTools.BookingDetails; +import com.xs.ai.services.FlightBookingService; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + + +@RestController +@CrossOrigin +public class BookingController { + + private final FlightBookingService flightBookingService; + + public BookingController(FlightBookingService flightBookingService) { + this.flightBookingService = flightBookingService; + } + @CrossOrigin + @GetMapping(value = "/booking/list") + public List getBookings() { + return flightBookingService.getBookings(); + } + +} diff --git a/src/main/java/com/xs/ai/controller/OpenAiController.java b/src/main/java/com/xs/ai/controller/OpenAiController.java new file mode 100644 index 0000000..8e79916 --- /dev/null +++ b/src/main/java/com/xs/ai/controller/OpenAiController.java @@ -0,0 +1,91 @@ +package com.xs.ai.controller; + +import com.xs.ai.services.LoggingAdvisor; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor; +import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +import java.time.LocalDate; + +import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY; +import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY; + + +/** + * @author xushu + * @version 1.0 + * @description: + */ +@RestController +@CrossOrigin +public class OpenAiController { + + private final ChatClient chatClient; + + public OpenAiController(ChatClient.Builder chatClientBuilder, VectorStore vectorStore, ChatMemory chatMemory) { + this.chatClient = chatClientBuilder + .defaultSystem(""" + 您是“Tuling”航空公司的客户聊天支持代理。请以友好、乐于助人且愉快的方式来回复。 + 您正在通过在线聊天系统与客户互动。 + 在提供有关预订或取消预订的信息之前,您必须始终 + 从用户处获取以下信息:预订号、客户姓名。 + 在询问用户之前,请检查消息历史记录以获取此信息。 + 在更改或退订之前,请先获取预订信息并且告知条款待用户回复确定之后才进行更改或退订的function-call。 + 请讲中文。 + 今天的日期是 {current_date}. + """) + .defaultAdvisors( + new PromptChatMemoryAdvisor(chatMemory), + new QuestionAnswerAdvisor(vectorStore, SearchRequest.query("预定航班")), // RAG + new LoggingAdvisor()) + .defaultFunctions("getBookingDetails", "changeBooking", "cancelBooking") // FUNCTION CALLING + .build(); + + + } + + + @CrossOrigin + @GetMapping(value = "/ai/generateStreamAsString", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux generateStreamAsString(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) { + //Prompt prompt = new Prompt(new UserMessage(message)); + //return chatClient.stream(prompt); + Flux content = chatClient.prompt() + .system(s -> s.param("current_date", LocalDate.now().toString())) + //.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)) + .advisors(a -> a.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)) + .user(message) + .stream() + .content(); + + return content + .concatWith(Flux.just("[complete]")); + + } + + + @Autowired + ChatClient.Builder chatClientBuilder; + @CrossOrigin + @GetMapping(value = "/ai/chat") + public String chat(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) { + + return chatClient.prompt() + .system(s -> s.param("current_date", LocalDate.now().toString())) + .user(message) + .call() + .content(); + + } + +} diff --git a/src/main/java/com/xs/ai/data/Booking.java b/src/main/java/com/xs/ai/data/Booking.java new file mode 100644 index 0000000..421c7f7 --- /dev/null +++ b/src/main/java/com/xs/ai/data/Booking.java @@ -0,0 +1,98 @@ +package com.xs.ai.data; + +import java.time.LocalDate; + +public class Booking { + + private String bookingNumber; + + private LocalDate date; + + private LocalDate bookingTo; + + private Customer customer; + + private String from; + + private String to; + + private BookingStatus bookingStatus; + + private BookingClass bookingClass; + + public Booking(String bookingNumber, LocalDate date, Customer customer, BookingStatus bookingStatus, String from, + String to, BookingClass bookingClass) { + this.bookingNumber = bookingNumber; + this.date = date; + this.customer = customer; + this.bookingStatus = bookingStatus; + this.from = from; + this.to = to; + this.bookingClass = bookingClass; + } + + public String getBookingNumber() { + return bookingNumber; + } + + public void setBookingNumber(String bookingNumber) { + this.bookingNumber = bookingNumber; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public LocalDate getBookingTo() { + return bookingTo; + } + + public void setBookingTo(LocalDate bookingTo) { + this.bookingTo = bookingTo; + } + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public BookingStatus getBookingStatus() { + return bookingStatus; + } + + public void setBookingStatus(BookingStatus bookingStatus) { + this.bookingStatus = bookingStatus; + } + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public String getTo() { + return to; + } + + public void setTo(String to) { + this.to = to; + } + + public BookingClass getBookingClass() { + return bookingClass; + } + + public void setBookingClass(BookingClass bookingClass) { + this.bookingClass = bookingClass; + } + +} \ No newline at end of file diff --git a/src/main/java/com/xs/ai/data/BookingClass.java b/src/main/java/com/xs/ai/data/BookingClass.java new file mode 100644 index 0000000..4f5b213 --- /dev/null +++ b/src/main/java/com/xs/ai/data/BookingClass.java @@ -0,0 +1,7 @@ +package com.xs.ai.data; + +public enum BookingClass { + + ECONOMY, PREMIUM_ECONOMY, BUSINESS + +} diff --git a/src/main/java/com/xs/ai/data/BookingData.java b/src/main/java/com/xs/ai/data/BookingData.java new file mode 100644 index 0000000..50e5786 --- /dev/null +++ b/src/main/java/com/xs/ai/data/BookingData.java @@ -0,0 +1,28 @@ +package com.xs.ai.data; + +import java.util.ArrayList; +import java.util.List; + +public class BookingData { + + private List customers = new ArrayList<>(); + + private List bookings = new ArrayList<>(); + + public List getCustomers() { + return customers; + } + + public void setCustomers(List customers) { + this.customers = customers; + } + + public List getBookings() { + return bookings; + } + + public void setBookings(List bookings) { + this.bookings = bookings; + } + +} diff --git a/src/main/java/com/xs/ai/data/BookingStatus.java b/src/main/java/com/xs/ai/data/BookingStatus.java new file mode 100644 index 0000000..a82a604 --- /dev/null +++ b/src/main/java/com/xs/ai/data/BookingStatus.java @@ -0,0 +1,7 @@ +package com.xs.ai.data; + +public enum BookingStatus { + + CONFIRMED, COMPLETED, CANCELLED + +} diff --git a/src/main/java/com/xs/ai/data/Customer.java b/src/main/java/com/xs/ai/data/Customer.java new file mode 100644 index 0000000..949265f --- /dev/null +++ b/src/main/java/com/xs/ai/data/Customer.java @@ -0,0 +1,35 @@ +package com.xs.ai.data; + +import java.util.ArrayList; +import java.util.List; + +public class Customer { + + private String name; + + private List bookings = new ArrayList<>(); + + public Customer() { + } + + public Customer(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getBookings() { + return bookings; + } + + public void setBookings(List bookings) { + this.bookings = bookings; + } + +} \ No newline at end of file diff --git a/src/main/java/com/xs/ai/services/BookingTools.java b/src/main/java/com/xs/ai/services/BookingTools.java new file mode 100644 index 0000000..1c284cc --- /dev/null +++ b/src/main/java/com/xs/ai/services/BookingTools.java @@ -0,0 +1,72 @@ +package com.xs.ai.services; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.xs.ai.data.BookingStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Description; +import org.springframework.core.NestedExceptionUtils; + +import java.time.LocalDate; +import java.util.function.Function; + +@Configuration +public class BookingTools { + + private static final Logger logger = LoggerFactory.getLogger(BookingTools.class); + + @Autowired + private FlightBookingService flightBookingService; + + public record BookingDetailsRequest(String bookingNumber, String name) { + } + + public record ChangeBookingDatesRequest(String bookingNumber, String name, String date, String from, String to) { + } + + public record CancelBookingRequest(String bookingNumber, String name) { + } + + @JsonInclude(Include.NON_NULL) + public record BookingDetails(String bookingNumber, String name, LocalDate date, BookingStatus bookingStatus, + String from, String to, String bookingClass) { + } + + @Bean + @Description("获取机票预定详细信息") + public Function getBookingDetails() { + return request -> { + try { + return flightBookingService.getBookingDetails(request.bookingNumber(), request.name()); + } + catch (Exception e) { + logger.warn("Booking details: {}", NestedExceptionUtils.getMostSpecificCause(e).getMessage()); + return new BookingDetails(request.bookingNumber(), request.name(), null, null, null, null, null); + } + }; + } + + @Bean + @Description("修改机票预定日期") + public Function changeBooking() { + return request -> { + flightBookingService.changeBooking(request.bookingNumber(), request.name(), request.date(), request.from(), + request.to()); + return ""; + }; + } + + @Bean + @Description("取消机票预定") + public Function cancelBooking() { + return request -> { + flightBookingService.cancelBooking(request.bookingNumber(), request.name()); + return ""; + }; + } + +} diff --git a/src/main/java/com/xs/ai/services/CustomerSupportAssistant.java b/src/main/java/com/xs/ai/services/CustomerSupportAssistant.java new file mode 100644 index 0000000..a5f8769 --- /dev/null +++ b/src/main/java/com/xs/ai/services/CustomerSupportAssistant.java @@ -0,0 +1,84 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xs.ai.services; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor; +import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.time.LocalDate; + +import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY; +import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY; + +/** + * * @author Christian Tzolov + */ +@Service +public class CustomerSupportAssistant { + + private final ChatClient chatClient; + + public CustomerSupportAssistant(ChatClient.Builder modelBuilder, VectorStore vectorStore, ChatMemory chatMemory) { + + // @formatter:off + this.chatClient = modelBuilder + .defaultSystem(""" + 您是“Funnair”航空公司的客户聊天支持代理。请以友好、乐于助人且愉快的方式来回复。 + 您正在通过在线聊天系统与客户互动。 + 在提供有关预订或取消预订的信息之前,您必须始终 + 从用户处获取以下信息:预订号、客户姓名。 + 在询问用户之前,请检查消息历史记录以获取此信息。 + 在更改预订之前,您必须确保条款允许这样做。 + 如果更改需要收费,您必须在继续之前征得用户同意。 + 使用提供的功能获取预订详细信息、更改预订和取消预订。 + 如果需要,可以调用相应函数调用完成辅助动作。 + 请讲中文。 + 今天的日期是 {current_date}. + """) + .defaultAdvisors( + new PromptChatMemoryAdvisor(chatMemory), // Chat Memory + // new VectorStoreChatMemoryAdvisor(vectorStore)), + + new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()), // RAG + // new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults() + // .withFilterExpression("'documentType' == 'terms-of-service' && region in ['EU', 'US']")), + + new LoggingAdvisor()) + + .defaultFunctions("getBookingDetails", "changeBooking", "cancelBooking") // FUNCTION CALLING + + .build(); + // @formatter:on + } + + public Flux chat(String chatId, String userMessageContent) { + + return this.chatClient.prompt() + .system(s -> s.param("current_date", LocalDate.now().toString())) + .user(userMessageContent) + .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)) + .stream() + .content(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/xs/ai/services/FlightBookingService.java b/src/main/java/com/xs/ai/services/FlightBookingService.java new file mode 100644 index 0000000..143544c --- /dev/null +++ b/src/main/java/com/xs/ai/services/FlightBookingService.java @@ -0,0 +1,101 @@ +package com.xs.ai.services; + + +import com.xs.ai.data.*; +import org.springframework.stereotype.Service; +import com.xs.ai.services.BookingTools.BookingDetails; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +@Service +public class FlightBookingService { + + private final BookingData db; + + public FlightBookingService() { + db = new BookingData(); + initDemoData(); + } + + private void initDemoData() { + List names = List.of("徐庶", "诸葛", "百里", "楼兰", "庄周"); + List airportCodes = List.of("北京", "上海", "广州", "深圳", "杭州", "南京", "青岛", "成都", "武汉", "西安", "重庆", "大连", + "天津"); + Random random = new Random(); + + var customers = new ArrayList(); + var bookings = new ArrayList(); + + for (int i = 0; i < 5; i++) { + String name = names.get(i); + String from = airportCodes.get(random.nextInt(airportCodes.size())); + String to = airportCodes.get(random.nextInt(airportCodes.size())); + BookingClass bookingClass = BookingClass.values()[random.nextInt(BookingClass.values().length)]; + Customer customer = new Customer(); + customer.setName(name); + + LocalDate date = LocalDate.now().plusDays(2 * (i + 1)); + + Booking booking = new Booking("10" + (i + 1), date, customer, BookingStatus.CONFIRMED, from, to, + bookingClass); + customer.getBookings().add(booking); + + customers.add(customer); + bookings.add(booking); + } + + // Reset the database on each start + db.setCustomers(customers); + db.setBookings(bookings); + } + + // 获取所有已预订航班 + public List getBookings() { + return db.getBookings().stream().map(this::toBookingDetails).toList(); + } + + // 根据编号+姓名查询航班 + private Booking findBooking(String bookingNumber, String name) { + return db.getBookings() + .stream() + .filter(b -> b.getBookingNumber().equalsIgnoreCase(bookingNumber)) + .filter(b -> b.getCustomer().getName().equalsIgnoreCase(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Booking not found")); + } + + // 查询航班详情 + public BookingDetails getBookingDetails(String bookingNumber, String name) { + var booking = findBooking(bookingNumber, name); + return toBookingDetails(booking); + } + + // 更改预定航班 + public void changeBooking(String bookingNumber, String name, String newDate, String from, String to) { + var booking = findBooking(bookingNumber, name); + if (booking.getDate().isBefore(LocalDate.now().plusDays(1))) { + throw new IllegalArgumentException("Booking cannot be changed within 24 hours of the start date."); + } + booking.setDate(LocalDate.parse(newDate)); + booking.setFrom(from); + booking.setTo(to); + } + + // 取消预定航班 + public void cancelBooking(String bookingNumber, String name) { + var booking = findBooking(bookingNumber, name); + if (booking.getDate().isBefore(LocalDate.now().plusDays(2))) { + throw new IllegalArgumentException("Booking cannot be cancelled within 48 hours of the start date."); + } + booking.setBookingStatus(BookingStatus.CANCELLED); + } + + private BookingDetails toBookingDetails(Booking booking) { + return new BookingDetails(booking.getBookingNumber(), booking.getCustomer().getName(), booking.getDate(), + booking.getBookingStatus(), booking.getFrom(), booking.getTo(), booking.getBookingClass().toString()); + } + +} diff --git a/src/main/java/com/xs/ai/services/LoggingAdvisor.java b/src/main/java/com/xs/ai/services/LoggingAdvisor.java new file mode 100644 index 0000000..2c56d0c --- /dev/null +++ b/src/main/java/com/xs/ai/services/LoggingAdvisor.java @@ -0,0 +1,20 @@ +package com.xs.ai.services; + +import org.springframework.ai.chat.client.RequestResponseAdvisor; +import org.springframework.ai.chat.client.advisor.api.AdvisedRequest; + +import java.util.Map; + +public class LoggingAdvisor implements RequestResponseAdvisor { + + @Override + public AdvisedRequest adviseRequest(AdvisedRequest request, Map context) { + System.out.println("Request: " + request); + return request; + } + + @Override + public int getOrder() { + return 0; + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..5a42f29 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,4 @@ + +spring.ai.dashscope.api-key= ${ALI_AI_KEY} +spring.ai.dashscope.chat.options.model=deepseek-v3 + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..54ed015 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,33 @@ +#xushu: +# # 官方(自行配置) +# openai: +# key: ${OPENAI_KEY} +# url: ${OPENAI_URL} +# # 中转1 +# aicore: +# key: ${OPEN_AI_KEY} +# url: ${OPEN_AI_URL} +# # 中转2 神经网络 算法模型,精通各个目标检测算法和熟悉神经网络基本架构 +# gaochao: +# key: ${OPEN_AI_KEY_GAOCHAO} +# url: ${OPEN_AI_URL_GAOCHAO} + + + +#spring: +# ai: +# openai: +# api-key: +# base-url: https://api.deepseek.com +# chat: +# options: +# model: deepseek-chat + + +spring: + ai: + ollama: + embedding: + model: shaw/dmeta-embedding-zh + chat: + model: deepseek-r1:8b \ No newline at end of file diff --git a/src/main/resources/rag/terms-of-service.txt b/src/main/resources/rag/terms-of-service.txt new file mode 100644 index 0000000..3e73bc8 --- /dev/null +++ b/src/main/resources/rag/terms-of-service.txt @@ -0,0 +1,13 @@ +本服务条款适用于您对 Funnair 的体验。预订航班,即表示您同意这些条款。 +1. 预订航班 +- 通过我们的网站或移动应用程序预订。 +- 预订时需要全额付款。 +- 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生 25 的费用。 +2. 更改预订 +- 允许在航班起飞前 24 小时更改。 +- 通过在线更改或联系我们的支持人员。 +- 改签费:经济舱 50,豪华经济舱 30,商务舱免费。 +3. 取消预订 +- 最晚在航班起飞前 48 小时取消。 +- 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。 +- 退款将在 7 个工作日内处理。 \ No newline at end of file diff --git a/src/test/java/com/xs/ai/SpringAiDemoApplicationTests.java b/src/test/java/com/xs/ai/SpringAiDemoApplicationTests.java new file mode 100644 index 0000000..38d9186 --- /dev/null +++ b/src/test/java/com/xs/ai/SpringAiDemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.xs.ai; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SpringAiDemoApplicationTests { + + @Test + void contextLoads() { + } + +}