Java
快来分享你的内容吧~
- 昨天 14:16·产品经理
- 昨天 11:30·产品经理/** * day29:定义Worker类(姓名/工种/工龄/评分),创建2个师傅对象并打印信息。 * day30:给Worker加3种构造:无参/姓名+工种/全字段,分别创建对象验证;用this区分同名参数 * day31:给Worker加static计数器count,每创建一个对象+1,打印当前平台注册师傅总数 * day32:Worker字段全改private;setWorkYears里校验查看全文加油鸭:这段代码展现了扎实的Java面向对象编程能力!从Worker类的封装、多构造器设计、静态计数器,到管理系统的完整功能(增删查改、排序、统计),逻辑清晰、边界处理细致。特别是输入校验、菜单交互和容错提示都做得很到位,是学以致用的优秀范例!331分享
- 2 天前·产品经理
- 3 天前·产品经理/** * day29:定义Worker类(姓名/工种/工龄/评分),创建2个师傅对象并打印信息。 * day30:给Worker加3种构造:无参/姓名+工种/全字段,分别创建对象验证;用this区分同名参数 * day31:给Worker加static计数器count,每创建一个对象+1,打印当前平台注册师傅总数 * day32:Worker字段全改private;setWorkYears里校验查看全文加油鸭:太棒了!从day29到day35的渐进式设计逻辑清晰、封装完整,每一步都扎实落地,这种系统性学习和工程化思维超赞!231分享
- 4 天前·产品经理* day29:定义Worker类(姓名/工种/工龄/评分),创建2个师傅对象并打印信息。 * day30:给Worker加3种构造:无参/姓名+工种/全字段,分别创建对象验证;用this区分同名参数 * day31:给Worker加static计数器count,每创建一个对象+1,打印当前平台注册师傅总数 * day32:Worker字段全改private;setWorkYears里校验工龄不查看全文加油鸭:太棒了!从day29到day35的持续迭代,把封装、构造器、static计数器、输入校验、数组管理全练透了,这种系统性积累特别扎实!231分享
- 09-24 00:29·产品经理
- 09-23 17:34·Java后端
- 09-23 12:08·产品经理
开始写继承代码,准备翻越山丘,跨过继承、抽象、接口的分水岭
 ``` /** * 建Person类(联系人姓名/电话),Company(品牌名/地址/服务范围)、Worker(工种/工龄/评分)、Customer(房屋地址/面积)继承它, * 创建智能家居报价系统角色对象测试 */ public class Day36SmartHomeQuotationTest { public static void main(String[] args){ Day36Person[] user = new Day36Person[5]; user[0] = new Day36Company("张经理","13829871635","视声智能","广州市黄埔区","KNX全屋智能/灯光/安防"); user[1] = new Day36Company("李经理","13150432864","杜亚窗帘","杭州市拱墅区","智能窗帘/智能门锁"); user[2] = new Day36Worker("王强","18125664636","灯光设计师",3,4.5); user[3] = new Day36Worker("陈明昊","13712570240","智能设计师",5,4.8); user[4] = new Day36Customer("马思纯","15138473848","上海市静安区",533); for (Day36Person per : user){ System.out.println("----------".repeat(10)); System.out.println(per); System.out.println(per.introduce()); } } } ``` ``` **Person类(父类)** public class Day36Person { private static final String PHONE_REGEX = "^1[3-9]\\d{9}$"; private static final String ERROR_PHONE_FORMAT = "手机号码格式不正确,已置空!"; private String name = ""; private String phone = ""; public Day36Person() { } public Day36Person(String name, String phone) { setName(name); setPhone(phone); } public void setName(String name) { this.name = name; } public void setPhone(String phone) { if (phone != null && phone.matches(PHONE_REGEX) ){ this.phone = phone; }else{ System.out.println(ERROR_PHONE_FORMAT); this.phone = ""; } } public String getName() { return this.name; } public String getPhone() { return this.phone; } @Override public String toString() { return "姓名:" + this.getName() + ",电话:" + this.getPhone(); } public String introduce(){ return ""; } } ``` ``` **Company类,继承Person类** public class Day36Company extends Day36Person { private String companyName = ""; private String address = ""; private String serviceScope = ""; public Day36Company() { } public Day36Company(String name, String phone, String companyName, String address, String serviceScope) { super(name, phone); setCompanyName(companyName); setAddress(address); setServiceScope(serviceScope); } public void setCompanyName(String companyName) { this.companyName = companyName; } public void setAddress(String address) { this.address = address; } public void setServiceScope(String serviceScope) { this.serviceScope = serviceScope; } public String getCompanyName() { return this.companyName; } public String getAddress() { return this.address; } public String getServiceScope() { return this.serviceScope; } @Override public String toString() { return "【公司】联系人:" + this.getName() + ",电话:" + this.getPhone() + ",品牌:" + this.getCompanyName() + ",地址:" + this.getAddress() + ",服务范围:" + this.getServiceScope(); } public String introduce() { StringBuilder sb = new StringBuilder(); sb.append("您好,我们是").append(this.getCompanyName()).append(",公司位于").append(this.getAddress()).append(",主营").append(this.getServiceScope()).append(",联系人").append(this.getName()); return sb.toString(); } } ``` ``` **Work类,继承Person类** public class Day36Worker extends Day36Person { private static final String ERROR_WORK_YEAR = "工龄0-70,其他无效!已置0"; private static final String ERROR_SCORE = "评分只能0-5,整数或一位小数!已置0"; private String jobType = ""; private int workYear = 0; private double score = 0.0; public Day36Worker() { } public Day36Worker(String name, String phone, String jobType, int workYear, double score) { super(name, phone); setJobType(jobType); setWorkYear(workYear); setScore(score); } public void setJobType(String jobType) { this.jobType = jobType; } public void setWorkYear(int workYear) { if (workYear >= 0 && workYear <= 70) { this.workYear = workYear; } else { System.out.println(ERROR_WORK_YEAR); this.workYear = 0; } } public void setScore(double score) { if (score >= 0 && score <= 5) { this.score = score; } else { System.out.println(ERROR_SCORE); this.score = 0.0; } } public String getJobType() { return this.jobType; } public int getWorkYear() { return this.workYear; } public double getScore() { return this.score; } @Override public String toString() { return "【师傅】:" + this.getName() + "," + this.getPhone() + "," + this.getJobType() + "," + this.getWorkYear() + "," + this.getScore(); } public String introduce() { StringBuilder sb = new StringBuilder(); sb.append("我是").append(this.getJobType()).append(",从业").append(this.getWorkYear()).append("年,评分").append(this.getScore()).append(",电话").append(this.getPhone()); return sb.toString(); } } ``` ``` **Customer类,继承Person类** public class Day36Customer extends Day36Person { private static final String ERROR_HOUSE_AREA = "面积大于0小于5000,整数或一位小数!"; private String houseAddress = ""; private double houseArea = 0.0; public Day36Customer() { } public Day36Customer(String name, String phone, String houseAddress, double houseArea) { super(name, phone); setHouseAddress(houseAddress); setHouseArea(houseArea); } public void setHouseAddress(String houseAddress) { this.houseAddress = houseAddress; } public void setHouseArea(double houseArea) { if (houseArea > 0.0 && houseArea < 5000.0) { this.houseArea = houseArea; } else { System.out.println(ERROR_HOUSE_AREA); this.houseArea = 0.0; } } public String getHouseAddress() { return this.houseAddress; } public double getHouseArea() { return this.houseArea; } @Override public String toString() { return "【业主】:" + this.getName() + "," + this.getPhone() + "," + this.getHouseAddress() + "," + this.getHouseArea() + "m²"; } public String introduce() { StringBuilder sb = new StringBuilder(); sb.append("业主").append(this.getName()).append(",房屋面积").append(this.getHouseArea()).append("m²,位于").append(this.getHouseAddress()).append(",需要智能家居报价。"); return sb.toString(); } } ```
企业级代码不是人写的,合并了升序降序的排序方法
```import java.util.Scanner; /** * day29:定义Worker类(姓名/工种/工龄/评分),创建2个师傅对象并打印信息。 * day30:给Worker加3种构造:无参/姓名+工种/全字段,分别创建对象验证;用this区分同名参数 * day31:给Worker加static计数器count,每创建一个对象+1,打印当前平台注册师傅总数 * day32:Worker字段全改private;setWorkYears里校验工龄不能<0(负数就置0或抛提示),setScore校验0-5分 * day33:师傅管理小程序:Worker数组容量10,支持添加、按名字查找、打印全部(遍历调用toString前先自己拼信息) * day34:在day33基础上加:按工种筛选、按评分排序、统计平均工龄,全部封装成方法 * day35:默写Worker类(封装+构造器+计数器版) * * <p> * 菜单:1-增加师傅 |2-按名字模糊查找 |3-打印全部 |4-按工种筛选 |5-按评分排序 |6-统计平均工龄 |exit-退出 * 方法: * 1. addWorker() * 2. searchWorker() * 3. printAllWorkers() * 4. filterByJobType() * 5. sortByScore() * 6. doByScore() * 7. calcAvgWorkYears() * 8. inputtedName() * 9. inputtedJobType() * 10. inputtedWorkYears() * 11. inputtedScore() * 12. getMenuItemNumber() * 13. judgeInput() */ public class Day35WorkerTest { // 定义程序、菜单、工种字典 private static final String APP_TITLE = "师傅管理小程序"; private static final String APP_LINE = "----------"; private static final String MENU_TIP = "1-增加师傅 |2-按名字模糊查找 |3-打印全部 |4-按工种筛选 |5-按评分排序 |6-统计平均工龄 |exit-退出"; private static final String MENU_INPUT_TIP = "请输入您的选择:"; private static final String EXIT_KEY_WORD = "EXIT"; private static final String EXIT_TIP = "Bye. thank you."; private static final String[] JOB_TYPE_DICT = {"电工", "泥工", "瓦工", "木工", "美缝工", "油漆工", "监理", "空调安装工", "智能调试"}; private static final String PRINT_ALL_WORKERS_TITLE = "打印全部师傅信息"; private static final String PRINT_NO_DATA = "没有记录,无法打印!"; private static final String CALCULATE_AVERAGE_WORK_YEARS_TITLE = "统计平均工龄"; private static final String CALCULATE_AVERAGE_WORK_YEARS_NO_DATA = "没有记录,无法统计!"; private static final String PRINT_AVERAGE_WORK_YEARS_TIP = "师傅平均工龄是:"; private static final String SEARCH_WORKER_TITLE = "按照姓名模糊查找"; private static final String SEARCH_WORKER_NO_DATA = "没有记录,无法查找!"; private static final String PRINT_SEARCH_WORKER_TIP = "查找到的记录有:"; private static final String SORT_BY_SCORE_TITLE = "按评分排序"; private static final String SORT_BY_SCORE_NO_DATA = "没有记录,无法排序!"; private static final String FILTER_BY_JOB_TYPE_TITLE = "按工种筛选"; private static final String FILTER_BY_JOB_TYPE_NO_DATA = "没有记录,无法筛选!"; // 定义输入的提示词 private static final String INPUT_NAME_TIP = "请输入姓名【exit-返回】:"; private static final String INPUT_JOB_TYPE_TIP = "请输入工种【exit-返回】:"; private static final String INPUT_WORK_YEARS_TIP = "请输入工龄(0-70)【exit-返回】:"; private static final String INPUT_SCORE_TIP = "请输入评分(0-5)【exit-返回】:"; private static final String INPUT_SORT_BY_SCORE = "请选择排序方式(1-升序 |2-降序)【exit-返回】:"; private static final String PREFIX_FILTER_BY_JOB_TYPE_TIP = "请选择工种筛选的序号"; private static final String SUFFIX_FILTER_BY_JOB_TYPE_TIP = "【exit-返回】):"; // 定义其他提示词 private static final String INPUT_IS_BLANK_TIP = "空的输入,请重新输入!"; private static final String INPUT_IS_INVALID_TIP = "无效的输入,请重新输入!"; private static final String MSG_JOB_TYPE_NOT_EXIST = "该工种暂未收录,已记录上报,审核通过后全平台可用"; private static final String ERROR_OUT_OF_RANGE_WORK_YEARS = "工龄不能小于 0 或 大于 70 !"; private static final String ERROR_OUT_OF_RANGE_SCORE = "评分不能小于 0 或 大于 5 !"; private static final String ERROR_OUT_OF_SIZE_WORKERS = "数据已满仓,无法添加!"; private static final String DATA_SAVED_SUCCESSFULLY = "数据已成功保存!"; // 定义正则(主菜单、工龄、评分、工种筛选菜单) private static final String MAIN_MENU_ITEM_NUMBER_REGEX = "[1-6]"; private static final String WORK_YEARS_REGEX = "\\d+"; private static final String SCORE_REGEX = "[0-5](\\.[0-9])?"; private static final String SORT_BY_SCORE_REGEX = "[1-2]"; // 定义哨兵值:-1(exit) -2(isBlank) -3(invalid) private static final int SENTINEL_IS_EXIT = -1; private static final int SENTINEL_IS_INVALID = -3; // 定义用户数组大小、增加用户的计数器(count 是"名册"的状态,不是"师傅"的属性,群体计数是看管数组的那一方的事) private static final int WORKERS_SIZE = 10; private static Worker35[] workers = new Worker35[WORKERS_SIZE]; private static int addWorkerCount = 0; // 模糊查找用户的计数器降级(降级为searchWorker局部变量,每次搜索清零、不跨方法存活,没资格当全局) public static void main(String[] args) { Scanner in = new Scanner(System.in); while (true) { System.out.println(APP_TITLE); System.out.println(APP_LINE.repeat(6)); System.out.println(MENU_TIP); System.out.print(MENU_INPUT_TIP); String inputtedStr = judgeInput(in); if (inputtedStr == null) { System.out.println(EXIT_TIP); break; } int menuItemNumber = getMenuItemNumber(inputtedStr, MAIN_MENU_ITEM_NUMBER_REGEX); switch (menuItemNumber) { case 1 -> { addWorker(in); } case 2 -> { searchWorker(in); } case 3 -> { printAllWorkers(); } case 4 -> { filterByJobType(in); } case 5 -> { sortByScore(in); } case 6 -> { calcAvgWorkYears(); } default -> { } } ; } } /** * 增加师傅 * * @param in 键盘输入 */ public static void addWorker(Scanner in) { // 1. 判断是否满员,是就返回 if (addWorkerCount == WORKERS_SIZE) { System.out.println(ERROR_OUT_OF_SIZE_WORKERS); return; } // 2. 调用方法 String name = inputtedName(in); if (name == null) { return; } String jobType = inputtedJobType(in); if (jobType == null) { return; } int workYears = inputtedWorkYears(in); if (workYears == SENTINEL_IS_EXIT) { return; } double score = inputtedScore(in); if (score == SENTINEL_IS_EXIT) { return; } workers[addWorkerCount] = new Worker35(name, jobType, workYears, score); addWorkerCount++; System.out.println(APP_LINE.repeat(6)); System.out.println(DATA_SAVED_SUCCESSFULLY); System.out.println("当前有:" + addWorkerCount + " 位师傅."); System.out.println(APP_LINE.repeat(6)); System.out.println(workers[addWorkerCount - 1]); } /** * 按姓名模糊查找 * * @param in 键盘输入 */ public static void searchWorker(Scanner in) { int foundCount = 0; // 找到符合的记录计数器 int[] foundIdxs = new int[addWorkerCount]; // 存放符合的记录的下标 System.out.print(APP_LINE.repeat(3)); System.out.print(SEARCH_WORKER_TITLE); System.out.print(APP_LINE.repeat(3) + "\n"); if (addWorkerCount == 0) { // 1. 判断是否有记录,无就提示并返回 System.out.println(SEARCH_WORKER_NO_DATA); } else { // 2. judgeInput() System.out.print(INPUT_NAME_TIP); String inputStr = inputtedName(in); if (inputStr == null) { // 3. 拦截 exit,避免 contains(null) return; } // 4. 获取输入,String.contains()或String.indexOf() for (int i = 0; i < addWorkerCount; i++) { if (workers[i].getName().contains(inputStr)) { // 5. 有符合的记录就保存下标,并继续查找 foundIdxs[foundCount] = i; foundCount++; } } // 4.1 查找到符合记录的计数器 System.out.print(PRINT_SEARCH_WORKER_TIP + foundCount + " 条.\n"); if (foundCount > 0) { for (int j = 0; j < foundCount; j++) { // 4.2 根据保存的下标,打印记录 System.out.println("[第 " + (j + 1) + " 条记录] " + workers[foundIdxs[j]]); } } } } /** * 打印全部师傅的信息,分为无记录提示,有记录打印 */ public static void printAllWorkers() { System.out.print(APP_LINE.repeat(3)); System.out.print(PRINT_ALL_WORKERS_TITLE); System.out.print(APP_LINE.repeat(3) + "\n"); if (addWorkerCount == 0) { // 没有记录 System.out.println(PRINT_NO_DATA); } else { // 有记录 for (int i = 0; i < addWorkerCount; i++) { System.out.println("[第 " + (i + 1) + " 条记录] " + workers[i]); } } } public static void filterByJobType(Scanner in) { int jobTypeCount = 0; // 不同工种的计数器 String[] jobTypeData = new String[addWorkerCount]; // 存放不同工种的数据 StringBuilder menu = new StringBuilder(); // 不同工种拼接 while (true) { System.out.print(APP_LINE.repeat(3)); System.out.print(FILTER_BY_JOB_TYPE_TITLE); System.out.println(APP_LINE.repeat(3)); // 1. 判断是否有记录,无就提示并返回 if (addWorkerCount == 0) { System.out.println(FILTER_BY_JOB_TYPE_NO_DATA); break; } // 2. 获取源记录中工种的种类,并生成菜单,拼接前缀 + 工种 + 后缀,以便用户选择 if (addWorkerCount == 1) { System.out.println("有 " + addWorkerCount + " 条记录"); System.out.println(workers[0]); break; } if (addWorkerCount > 1) { for (int i = 0; i < addWorkerCount; i++) { String type = workers[i].getJobType(); boolean isExist = false; // 检查jobTypeData[]是否存在 for (int j = 0; j < jobTypeCount; j++) { // 如果相同就退出 if (type.equalsIgnoreCase(jobTypeData[j])) { isExist = true; break; } } // 不相同就写入 if (!isExist) { jobTypeData[jobTypeCount++] = type; } } // 去重工种的数组生成菜单字符串 menu.setLength(0); for (int i = 0; i < jobTypeCount; i++) { menu.append(i + 1).append(" - ").append(jobTypeData[i]).append(" |"); } String menuStr = menu.toString(); System.out.print(PREFIX_FILTER_BY_JOB_TYPE_TIP + "(" + menuStr + SUFFIX_FILTER_BY_JOB_TYPE_TIP); String inputStr = judgeInput(in); // 预处理键盘输入 if (inputStr == null) { break; } // 生成菜单正则 String menuRegex = "[1-" + jobTypeCount + "]"; int menuItemNumber = getMenuItemNumber(inputStr, menuRegex); if ((menuItemNumber > jobTypeCount) || (menuItemNumber < 1)) { System.out.println(INPUT_IS_INVALID_TIP); continue; } // 按照选择的工种,遍历查找,找到就保存下标,继续查找 int[] foundIdxs = new int[addWorkerCount]; int foundCount = 0; for (int i = 0; i < addWorkerCount; i++) { if (workers[i].getJobType().equalsIgnoreCase(jobTypeData[menuItemNumber - 1])) { foundIdxs[foundCount++] = i; } } System.out.println("筛选到 " + foundCount + " 条数据"); for (int i = 0; i < foundCount; i++) { System.out.println(workers[foundIdxs[i]]); } break; } } } public static void sortByScore(Scanner in) { while (true) { System.out.print(APP_LINE.repeat(3)); System.out.print(SORT_BY_SCORE_TITLE); System.out.print(APP_LINE.repeat(3)); if (addWorkerCount == 0) { System.out.println(SORT_BY_SCORE_NO_DATA); break; } else { System.out.print("\n" + INPUT_SORT_BY_SCORE); String inputStr = judgeInput(in); // 1. 用户选择升序还是降序 if (inputStr == null) { return; } doByScore(inputStr); // 2. 按用户选择排序 break; } } } public static void doByScore(String sortOrderStr) { if (!sortOrderStr.matches(SORT_BY_SCORE_REGEX)) { System.out.println(INPUT_IS_INVALID_TIP); return; } int sortOrderNum = Integer.parseInt(sortOrderStr); boolean isAsc = (sortOrderNum == 1); // 升序或降序的排序标志 for (int i = 0; i < addWorkerCount - 1; i++) { for (int j = 0; j < addWorkerCount - 1 - i; j++) { double left = workers[j].getScore(); double right = workers[j + 1].getScore(); boolean needSwap = isAsc ? (left > right) : (left < right); if (needSwap) { Worker34 w = workers[j]; workers[j] = workers[j + 1]; workers[j + 1] = w; } } } // 排序完打印 printAllWorkers(); } /** * 计算全部师傅的平均工龄 */ public static void calcAvgWorkYears() { if (addWorkerCount == 0) { // 1. 判断是否有记录,无就提示并返回 System.out.println(CALCULATE_AVERAGE_WORK_YEARS_NO_DATA); } else { System.out.print(APP_LINE.repeat(3)); System.out.print(CALCULATE_AVERAGE_WORK_YEARS_TITLE); System.out.print(APP_LINE.repeat(3) + "\n"); double avgWorkYears = 0.0; // 2. 遍历计算平均值并返回 for (int i = 0; i < addWorkerCount; i++) { avgWorkYears += workers[i].getWorkYear(); } avgWorkYears = Math.round((avgWorkYears / addWorkerCount) * 10) / 10; // 3. 获取师傅的数量,计算平均值 System.out.println("当前有:" + addWorkerCount + " 位师傅."); System.out.println(PRINT_AVERAGE_WORK_YEARS_TIP + avgWorkYears + "年"); } } /** * 获取姓名 * * @param in 键盘输入 * @return 返回字符串 */ public static String inputtedName(Scanner in) { String inputtedStr; while (true) { System.out.print(INPUT_NAME_TIP); inputtedStr = judgeInput(in); if (inputtedStr == null) { break; } if (inputtedStr.isBlank()) { continue; } break; } return inputtedStr; } /** * 获取工种 * * @param in 键盘输入 * @return 返回字符串 */ public static String inputtedJobType(Scanner in) { String inputtedStr; while (true) { System.out.print(INPUT_JOB_TYPE_TIP); inputtedStr = judgeInput(in); if (inputtedStr == null) { break; } if (inputtedStr.isBlank()) { System.out.println(INPUT_IS_BLANK_TIP); continue; } // 2. 获取输入,判断是否在字典里,不在就提示并重新输入 boolean isHit = false; for (String type : JOB_TYPE_DICT) { if (type.equalsIgnoreCase(inputtedStr)) { isHit = true; break; } } if (!isHit) { System.out.println(MSG_JOB_TYPE_NOT_EXIST); } else { return inputtedStr; } } return null; } /** * 获取工龄 * * @param in 键盘输入 * @return int,0-70 */ public static int inputtedWorkYears(Scanner in) { int inputtedNum = 0; while (true) { System.out.print(INPUT_WORK_YEARS_TIP); String inputStr = judgeInput(in); // 1. judgeInput() if (inputStr == null) { return SENTINEL_IS_EXIT; } if (inputStr.isBlank()) { System.out.println(INPUT_IS_BLANK_TIP); continue; } if (!inputStr.matches(WORK_YEARS_REGEX)) { // 2. 获取输入,正则(INT_WORK_YEARS_REGEX)判断,不符合就提示并重新输入 System.out.println(INPUT_IS_INVALID_TIP); } else { inputtedNum = Integer.parseInt(inputStr); if (inputtedNum < 0 || inputtedNum > 70) { // 3. 判断,小于0拦,大于70拦(0<= workYears <= 70放行) System.out.println(ERROR_OUT_OF_RANGE_WORK_YEARS); continue; } return inputtedNum; } } } /** * 获取评分 * * @param in 键盘输入 * @return double类型 0-5 */ public static double inputtedScore(Scanner in) { double inputtedNum = 0.0; while (true) { System.out.print(INPUT_SCORE_TIP); String inputStr = judgeInput(in); if (inputStr == null) { return SENTINEL_IS_EXIT; } if (inputStr.isBlank()) { System.out.println(INPUT_IS_BLANK_TIP); continue; } if (!inputStr.matches(SCORE_REGEX)) { // 2. 获取输入,正则(SCORE_REGEX)判断,不符合就提示并重新输入 System.out.println(INPUT_IS_INVALID_TIP); } else { inputtedNum = Double.parseDouble(inputStr); if (inputtedNum < 0 || inputtedNum > 5) { // 3. 判断,小于0拦,大于5拦(0<= score <= 5放行) System.out.println(ERROR_OUT_OF_RANGE_SCORE); continue; } return inputtedNum; } } } /** * 获取菜单编号,只接收1-6和 exit,其他无效 * * @param inputStr 调用方法 judgeInput()预处理后返回的字符串 * @param regex 菜单正则 * @return 返回菜单编号 */ public static int getMenuItemNumber(String inputStr, String regex) { // 1. judgeInput() // 2. 获取输入,正则判断,不符合就提示并重新输入 if (!inputStr.matches(regex)) { System.out.println(INPUT_IS_INVALID_TIP); return SENTINEL_IS_INVALID; } // 3. 返回菜单编号 return Integer.parseInt(inputStr); } /** * 预处理键盘输入,各个方法抽取出来的,不抢占原来方法的功能 * * @param in 键盘输入 * @return ”“、null、原串 */ public static String judgeInput(Scanner in) { String inputStr = in.nextLine().strip(); if (inputStr.isBlank()) { return ""; } if (EXIT_KEY_WORD.equalsIgnoreCase(inputStr)) { return null; } return inputStr; } } ``` ```/** * * day29:定义Worker类(姓名/工种/工龄/评分),创建2个师傅对象并打印信息。 * day30:给Worker加3种构造:无参/姓名+工种/全字段,分别创建对象验证;用this区分同名参数 * day31:给Worker加static计数器count,每创建一个对象+1,打印当前平台注册师傅总数 * day32:Worker字段全改private;setWorkYears里校验工龄不能<0(负数就置0或抛提示),setScore校验0-5分 * day33:师傅管理小程序:Worker数组容量10,支持添加、按名字查找、打印全部(遍历调用toString前先自己拼信息) * day34:在Day5基础上加:按工种筛选、按评分排序、统计平均工龄,全部封装成方法 * day35:默写Worker类(封装+构造器+计数器版) * */ class Worker35 { private String name; private String jobType; private int workYear; private double score; public Worker35(){this(null,null,0,0.0);} public Worker35(String name, String jobType){ this(name, jobType, 0, 0.0); } public Worker35(String name, String jobType, int workYear, double score){ this.setName(name); this.setJobType(jobType); this.setWorkYear(workYear); this.setScore(score); } public void setName(String name){ this.name = name; } public void setJobType(String jobType){ this.jobType = jobType; } public void setWorkYear(int workYear){ if (workYear < 0){ System.out.println("工龄不能小于0,已置0"); this.workYear = 0; }else{ this.workYear = workYear; } } public void setScore(double score){ if (!(score >= 0 && score <=5)){ System.out.println("评分不能小于0或大于5,已置0"); this.score = 0; }else{ this.score = score; } } public String getName(){ return this.name; } public String getJobType(){ return this.jobType; } public int getWorkYear(){ return this.workYear; } public double getScore(){ return this.score; } @Override public String toString(){ return "姓名:" + this.getName() + ",工种:" + this.getJobType() + ",工龄:" + this.getWorkYear() + ",评分:" + this.getScore(); } } ```
遥测数据生成仿真系统
 <p align="center">数据生成 · 协议编解码 · 实时推送 · 3D 可视化 · 虚拟卫星 · AI 助手 · 调试工具</p> 通用型遥测数据仿真平台,基于 Java 21 + Spring Boot 3.3、React 18 + TypeScript 前后端分离技术栈:从仿真造数、协议编解码、实时推送,到外部系统对接、3D 轨道可视化、虚拟卫星整星仿真与开发者调试,覆盖完整链路。 ## 一、界面预览 | 页面 | 截图位置 | 页面内容 | | ------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | | 控制台 |  | 链路状态横幅、系统运行指标卡、数据统计(最新/最大/最小/均值)、实时时序曲线 / 数值分布直方图 / 值-变化率散点 | | 虚拟卫星 |  | 整星大屏(测点磁贴 + quality 色带 + sparkline)、事件流、6 个任务场景时间线、轨道绑定、分发与历史回放面板 | | 数据仿真生成 |  | 六种生成模式参数表单(含脚本编辑器 / 模板 / 试运行)、实时数据预览、接收速率统计、Webhook 与导出 | | 协议配置 |  | 协议 CRUD 表单与 JSON / HEX / 二进制编解码调试 | | 3D 场景 |  | Orekit 轨道 3D 渲染(动态卫星 + 星下点地面轨迹)、地面轨迹经纬度图、设备拓扑 | | AI 助手 |  | 模型对话面板与八大场景化能力入口 | | 调试工具 |  | 报文调试、编解码、参数快照、导入导出与历史回放 | ## 二、系统架构 前后端分离 + 分层架构:前端 SPA 经 **REST 与 SSE 双通道**接入后端;后端以统一参数契约(`GenerateBaseDTO`)支撑**四条取数通路**(SSE 实时推送 / REST 批量拉取 / 文件导出 / Webhook 转发),并叠加**虚拟卫星独立域**(`/api/vsat/**`):  ```mermaid flowchart LR subgraph FE["前端 SPA · React 18 + TypeScript · Ant Design · Zustand · ECharts / three.js"] PAGES["7 大页面<br/>控制台 · 虚拟卫星 · 数据仿真生成<br/>协议配置 · 3D 场景 · AI 助手 · 调试工具"] end subgraph BE["后端 · Spring Boot 3.3 · Java 21"] API["Controller API 层<br/>simulation / sse / protocol / codec / orbit / ai / debug / open<br/>vsat:stream · replay · history · profile · scenario · client · dataset"] SVC["Service 层<br/>推送循环 · 取数导出 · 协议脚本 · 卫星编排"] GEN["数据生成引擎<br/>6 种模式 + Rhino 脚本"] ORB["Orekit 轨道计算<br/>采样 · 地面轨迹 · 光照"] CODEC["协议编解码<br/>JSON / HEX / Binary"] VENG["虚拟卫星引擎<br/>物理联动 · 帧流 · 场景 · 回放 · 分发"] AIF["AI 客户端工厂<br/>DeepSeek / 百炼 / 通用"] HOOK["Webhook 转发<br/>有界队列 · 尽力投递"] MAPPER["持久层 · MyBatis-Flex"] API --> SVC SVC --> GEN & ORB & CODEC & VENG & AIF & HOOK SVC --> MAPPER end DB[("PostgreSQL<br/>协议 / 快照 / 遥测记录 / 档案 / 测点<br/>TLE / 场景 / 留存段 / 帧 / 分发客户端")] EXT["外部业务系统<br/>批量拉取 · 文件导出 · Webhook 接收"] TLE["TLE 数据源<br/>CelesTrak · space-track · KeepTrack"] DIST["外部分发客户端<br/>X-API-Token · scopes"] AIP["外部 AI 服务<br/>DeepSeek / 百炼"] PAGES <-->|"REST /api/** ↔ SSE 事件流"| API EXT -->|"batch / export"| API GEN -->|"orbit 模式联动"| ORB MAPPER --> DB VENG -->|"TLE 绑定 · 降级链"| TLE VENG -->|"整星帧流 / 回放"| DIST AIF -->|"OpenAI 兼容调用"| AIP HOOK -->|"逐条 POST"| EXT classDef fe fill:#e0f2fe,stroke:#0284c7,stroke-width:1.5px,color:#0c4a6e classDef be fill:#ecfdf5,stroke:#059669,stroke-width:1.5px,color:#064e3b classDef eng fill:#fefce8,stroke:#ca8a04,stroke-width:1.5px,color:#713f12 classDef ext fill:#faf5ff,stroke:#9333ea,stroke-width:1.5px,color:#581c87 classDef db fill:#fff1f2,stroke:#e11d48,stroke-width:1.5px,color:#881337 class PAGES fe class API,SVC,MAPPER be class GEN,ORB,CODEC,VENG,AIF,HOOK eng class EXT,TLE,DIST,AIP ext class DB db ``` 设计要点: - **一套引擎、四条通路**:所有取数入口共用 `DataGenerator` 接口与工厂,生成器无需感知数据从哪条通路取出; - **推送可靠性**:SSE 断线且未配 Webhook 时自动停止任务;Webhook 走有界队列尽力投递(队满丢弃并计数,不阻塞生成主链路); - **鉴权分层**:开放接口可选 `X-API-Token`;虚拟卫星分发放发 token(SHA-256 哈希存储),按「卫星 × 接口」scopes 限权; - **独立域设计**:虚拟卫星以独立包 + 独立路由叠加,不复用、不侵入原有遥测数据通路。 ## 三、核心功能 ### 1. 数据生成(六种模式) - `random` 随机 / `fixed` 固定 / `wave` 周期波 / `anomaly` 异常注入 / `orbit` 轨道联动(Orekit 实时推演)/ `script` 脚本生成 - 完整参数:格式(json/hex/binary)、频率、精度、波动幅度、周期、上报时长、轨道六根数 - **script 模式**:基于 Rhino 1.7.15(纯 Java JavaScript 引擎) - 内置 6 套脚本模板:正弦波动 / 指数衰减 / 方波阶跃 / 高斯噪声 / 随机脉冲 / 多字段对象 - 可用变量 `tick / seconds / baseValue / waveOffset / anomalyRate / period`,内置函数 `rand / randRange / randInt / gauss / clamp` - `return` 数值或 `{value, field, unit}` 对象(自定义字段名与单位) - 启动前自动校验(语法错误提示行号)、前端「试运行」样例预览、死循环超时自动中断、运行期异常限流回退基线值不断流 - 脚本已禁用 Java 类访问,仅保留受限函数 ### 2. 协议编解码 - 全协议编解码器:JSON / HEX / 二进制(Base64) + 统一工厂(可扩展注册) - 编解码调试接口与前端表单双向验证:结构化数据 ⇄ 报文 ### 3. 实时推送与连接治理(SSE) - 心跳定时广播、注释帧握手(注册后立即 flush 响应头) - 连接监控 `/api/sse/status`、推送统计、任务治理(防重复启动 / 断线自动停止) - 前端会话为模块级单例:切页不断流,含断线重连 ### 4. 外部取数与转发 - 开放接口:批量拉取 `POST /api/open/data/batch`(≤10000 条)、文件导出 `POST /api/open/data/export`(CSV/JSONL,≤100000 条流式下载) - Webhook 实时转发:默认内置演示接收端(自环验证),可替换为任意 http/https 外部地址 - 数据通路支持 `X-API-Token` 可选鉴权 ### 5. 轨道与 3D 可视化 - Orekit 轨道周期采样、星下点地面轨迹(GTOD 近似地固系) - 3D 场景:动态卫星 + 轨道线 + 星下点贴地轨迹,配套地面轨迹经纬度图与设备拓扑链路图 - 后端基于 Orekit 12.1.1 + Hipparchus 3.1(版本严格匹配,见「版本与依赖说明」) ### 6. AI 助手(八大场景化能力) 数据纠错 / 异常模拟 / 数据补全 / 趋势分析 / 场景生成 / 协议解析 / 报错诊断 / 参数优化。 - 多 AI 统一接口 + 工厂:DeepSeek / 阿里百炼 / 通用 OpenAI 兼容服务 - 默认关闭(`telemetry.ai.enable=false`),配置密钥后在 dev/prod 环境开启 ### 7. 虚拟卫星数据服务(叠加式独立域 `/api/vsat/**`) - **档案与字典**:卫星档案(profiles)与字段字典(JSON/CSV 导出) - **物理联动引擎**:9+1 测点模型、光照判定双模式(precise 精确 / simple 简化),SSE 整星帧流(`vsat.frame` 事件,支持 rate/record/scenario 参数) - **任务场景**:6 个内置场景(相位故事线),场景时间线切换 - **真实 TLE 绑定**:CelesTrak / space-track / KeepTrack 多源绑定与降级链,轨道切换联动 - **历史与回放**:历史帧留存 / 分页查询 / 测点时序 / 导出,SSE 回放(同构实时,支持 from/to/speed/loop/seek 与按事件跳转) - **分发**:分发客户端 token 创建与用量统计(数据通路鉴权) - **整星大屏前端**:测点磁贴 + quality 色带 + sparkline、事件流、接入信息(SSE 地址 / 字段字典 / 三框架示例代码) ### 8. 调试工具 - 报文调试、参数快照(保存/列表/详情/删除)、批量导入导出、历史回放 ## 四、技术栈总览 | 端 | 技术栈 | | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 后端 | Java 21 · Spring Boot 3.3 · MyBatis-Flex · PostgreSQL · SSE · Webhook 转发 · Orekit 12.1.1 · Hutool · Rhino 1.7.15 · springdoc OpenAPI · DeepSeek/阿里百炼/通用 AI | | 前端 | React 18 + TypeScript · Vite 5 · Ant Design 6.6.5 · Zustand · react-force-graph-3d · @react-three/fiber · echarts-for-react | ## 五、项目结构 ``` tse/ ├── backend/ # 后端工程(Maven) └── frontend/ # 前端工程(Vite) ``` ### 后端 backend(分层结构) ``` backend/ ├── pom.xml # 依赖管理(Java21/MyBatis-Flex/Orekit/Rhino/Hutool/PostgreSQL) ├── src/main/resources/ │ ├── application.yml # 全局配置(含 spring.profiles.active) │ ├── application-dev.yml # 开发环境(含 AI 密钥占位) │ ├── application-test.yml # 测试环境 │ ├── application-prod.yml # 生产环境(密钥走环境变量) │ ├── db/schema.sql # 建表 SQL │ ├── mapper/ # MyBatis-Flex XML(可选) │ ├── orekit-data/ # Orekit 数据目录(随包携带 tai-utc.dat) │ └── vsat/profiles/ # 虚拟卫星档案定义 └── src/main/java/com/telemetry/sim/ ├── TelemetrySimApplication.java # 启动类 ├── common/ # 通用:result / exception / constant / enums ├── config/ # 配置:Web/CORS、Orekit、AI 客户端、全局 ├── controller/ # 控制层:protocol/sse/simulation/ai/orbit/open/debug/vsat ├── service/ + impl/ # 服务层:协议、SSE 推送 ├── mapper/ # 持久层(MyBatis-Flex BaseMapper) ├── entity/ # 模型层(表实体) ├── model/dto + vo # 数据传输对象 / 视图对象 ├── sse/ # SSE 连接管理器 ├── orekit/ # 轨道仿真统一封装(采样/地面轨迹/光照) ├── ai/ # 多 AI 统一接口 + DeepSeek/百炼/通用 + 工厂 ├── protocol/ parser + encoder # 协议解析 / 编码(可扩展) ├── generator/data/ # 数据生成器(接口 + 工厂 + 六种模式) ├── vsat/ # 虚拟卫星域(profile/physics/frame/history/distribute/orbitsource) ├── webhook/ # Webhook 转发与内置接收端 └── util/ # Hutool 统一封装入口 ``` ### 前端 frontend(Vite 工程) ``` frontend/ ├── package.json / vite.config.ts / tsconfig.json / index.html ├── public/ # 品牌资源(logo.png / favicon.svg) ├── .env.development / .env.production └── src/ ├── main.tsx / App.tsx / vite-env.d.ts ├── api/ # HTTP 封装 + 协议/仿真/AI/虚拟卫星接口 ├── router/ # 路由(控制台/虚拟卫星/数据仿真生成/协议配置/3D 场景/AI 助手/调试工具) ├── store/ # Zustand 状态模块(simulation / ai / vsat) ├── sse/ # SSE 客户端(单值流 + 整星帧流,均含断线重连) ├── types/ # TS 类型定义(simulation / orbit / vsat) ├── hooks/ utils/ # 自定义钩子与工具 ├── components/ # common/charts/three/force-graph/protocol/ai/vsat 组件 ├── pages/ # 7 大业务页面 └── styles/ # 全局样式 ``` ## 六、环境要求 - **后端**:JDK 21、Maven 3.9+、PostgreSQL(默认库名 `telemetry_sim`) - **前端**:Node.js 18+、npm/pnpm ## 七、快速启动 ### 后端 ```bash # 1. 创建数据库 psql -U postgres -c "CREATE DATABASE telemetry_sim;" # 2. 初始化表结构(协议配置 / 调试快照 / 遥测记录) psql -U postgres -d telemetry_sim -f src/main/resources/db/schema.sql # 3. 按需修改 application-dev.yml 中的数据库账号密码 # 4. 启动(默认端口 8080) cd backend mvn spring-boot:run ``` ### 前端 ```bash cd frontend npm install npm run dev # 默认 http://localhost:5173,/api 已代理到后端 8080 ``` ## 八、接口文档索引 - 在线接口文档(Swagger UI):`http://localhost:8080/swagger-ui.html`(OpenAPI JSON:`/v3/api-docs`;分组 `default` / `open-api`;生产环境默认关闭) ## 九、核心接口一览 | 模块 | 方法与路径 | 说明 | | -------- | --------------------------------------- | ------------------------------------------------------- | | 协议 | POST`/api/protocol` | 新增协议配置 | | 协议 | GET`/api/protocol/page` | 分页查询 | | 协议 | GET`/api/protocol/codec/formats` | 已注册编解码格式 | | 协议 | POST`/api/protocol/codec/encode` | 编码:结构化数据 → 报文(json/hex/binary) | | 协议 | POST`/api/protocol/codec/decode` | 解码:报文 → 结构化数据 | | SSE | GET`/api/sse/connect/{clientId}` | 建立 SSE 连接(EventSource) | | SSE | GET`/api/sse/status` | 连接监控与推送统计 | | 仿真 | POST`/api/simulation/start` | 启动推送(可携带 webhookUrl 实时转发外部系统) | | 仿真 | POST`/api/simulation/stop/{clientId}` | 停止推送 | | 仿真 | GET`/api/simulation/script/templates` | 内置脚本模板列表(script 模式) | | 仿真 | POST`/api/simulation/script/validate` | 脚本校验与试运行(返回样例数据) | | 轨道 | GET`/api/orbit/position` | Orekit 轨道位置计算 | | 轨道 | GET`/api/orbit/sample` | 轨道周期采样(3D 轨迹点) | | 轨道 | GET`/api/orbit/groundtrack` | 地面轨迹(星下点经纬度) | | AI | GET`/api/ai/models` | 可用模型列表 | | AI | POST`/api/ai/chat` | 模型对话 | | AI | GET`/api/ai/scenarios` | 场景化能力列表(八大场景) | | AI | POST`/api/ai/scenario` | 场景化能力执行 | | 调试 | POST`/api/debug/snapshot` | 保存参数快照 | | 调试 | GET`/api/debug/snapshot/list` | 快照列表 | | 调试 | GET/DELETE`/api/debug/snapshot/{id}` | 快照详情 / 删除 | | 开放 | POST`/api/open/data/batch` | 外部系统批量取数(≤10000 条,X-API-Token 可选鉴权) | | 开放 | POST`/api/open/data/export` | 文件导出 CSV / JSONL(≤100000 条,流式下载) | | 开放 | GET`/api/open/data/webhook/status` | Webhook 转发运行统计 | | 虚拟卫星 | GET`/api/vsat/stream/{profile}` | SSE 整星帧流(`vsat.frame` 事件;rate/record/scenario) | | 虚拟卫星 | GET`/api/vsat/replay/{profile}` | SSE 历史回放(与实时同构;from/to/speed/loop/seek) | | 虚拟卫星 | GET`/api/vsat/profiles` | 卫星档案列表(`/{code}/dictionary` 字段字典 JSON/CSV) | | 虚拟卫星 | GET`/api/vsat/history/{profile}/frames` | 历史帧分页(另有`points` 测点时序与 `export` 导出) | | 虚拟卫星 | POST`/api/vsat/clients` | 分发客户端 token 创建(数据通路 X-API-Token 鉴权) | ## 十、多环境与配置 通过 `spring.profiles.active`(application.yml)或启动参数切换: ```bash mvn spring-boot:run -Dspring-boot.run.profiles=dev # / test / prod ``` 生产环境数据库与 AI 密钥通过环境变量注入(`DB_HOST/DB_USERNAME/DB_PASSWORD/DEEPSEEK_API_KEY` 等),配置完全隔离。 ## 十一、版本与依赖说明 - 依赖版本集中于后端 `pom.xml` `<properties>` 与前端 `package.json`,便于统一升级。 - Orekit 与 Hipparchus 版本需严格匹配:Orekit `12.1.1` 对应 Hipparchus `3.1`(见 `pom.xml` 的 `hipparchus.version`)。 - 内置脚本生成基于 Rhino(纯 Java JavaScript 引擎),版本见 `pom.xml` 的 `rhino.version`(`1.7.15`);脚本已禁用 Java 类访问并带执行超时保护。 - Ant Design 按需求文档指定为 `6.6.5`;若该版本在私有仓库不可用时,可调整回 5.x 稳定版(需同步适配 API)。 - Orekit 数据目录解析优先级:环境变量 `OREKIT_DATA_DIR` → 配置项 `telemetry.orekit.data-dir` → classpath 内置 `src/main/resources/orekit-data/`(已随包携带 `tai-utc.dat` 闰秒文件,满足 UTC 时标 / EME2000 传播 / GTOD 简化星下点计算)→ 工作目录相对路径回退;目录缺失时应用仍可启动,仅 `/api/orbit` 轨道接口不可用。如需 EOP、JPL 星历等高级数据,可从 `https://gitlab.orekit.org/orekit/orekit-data` 获取后放入该目录或通过前两种方式指定外部目录。 - AI 能力默认关闭(`telemetry.ai.enable=false`),配置密钥后于 dev/prod 环境开启 ## 十二、开源地址 Gitee: https://gitee.com/zd_g/tes.git
企业级代码不是人写的,注释错了都要改
**排序方法需要合并** ```import java.util.Scanner; /** * day29:定义Worker类(姓名/工种/工龄/评分),创建2个师傅对象并打印信息。 * day30:给Worker加3种构造:无参/姓名+工种/全字段,分别创建对象验证;用this区分同名参数 * day31:给Worker加static计数器count,每创建一个对象+1,打印当前平台注册师傅总数 * day32:Worker字段全改private;setWorkYears里校验工龄不能<0(负数就置0或抛提示),setScore校验0-5分 * day33:师傅管理小程序:Worker数组容量10,支持添加、按名字查找、打印全部(遍历调用toString前先自己拼信息) * day34:在day33基础上加:按工种筛选、按评分排序、统计平均工龄,全部封装成方法 * day35:默写Worker类(封装+构造器+计数器版) * * <p> * 菜单:1-增加师傅 |2-按名字模糊查找 |3-打印全部 |4-按工种筛选 |5-按评分排序 |6-统计平均工龄 |exit-退出 * 方法: * 1. addWorker() * 2. searchWorker() * 3. printAllWorkers() * 4. filterByJobType() * 5. sortByScore() * 6. doByScore() * 7. calcAvgWorkYears() * 8. inputtedName() * 9. inputtedJobType() * 10. inputtedWorkYears() * 11. inputtedScore() * 12. getMenuItemNumber() * 13. judgeInput() */ public class Day34WorkerTest { // 定义程序、菜单、工种字典 private static final String APP_TITLE = "师傅管理小程序"; private static final String APP_LINE = "----------"; private static final String MENU_TIP = "1-增加师傅 |2-按名字模糊查找 |3-打印全部 |4-按工种筛选 |5-按评分排序 |6-统计平均工龄 |exit-退出"; private static final String MENU_INPUT_TIP = "请输入您的选择:"; private static final String EXIT_KEY_WORD = "EXIT"; private static final String EXIT_TIP = "Bye. thank you."; private static final String[] JOB_TYPE_DICT = {"电工", "泥工", "瓦工", "木工", "美缝工", "油漆工", "监理", "空调安装工", "智能调试"}; private static final String PRINT_ALL_WORKERS_TITLE = "打印全部师傅信息"; private static final String PRINT_NO_DATA = "没有记录,无法打印!"; private static final String CALCULATE_AVERAGE_WORK_YEARS_TITLE = "统计平均工龄"; private static final String CALCULATE_AVERAGE_WORK_YEARS_NO_DATA = "没有记录,无法统计!"; private static final String PRINT_AVERAGE_WORK_YEARS_TIP = "师傅平均工龄是:"; private static final String SEARCH_WORKER_TITLE = "按照姓名模糊查找"; private static final String SEARCH_WORKER_NO_DATA = "没有记录,无法查找!"; private static final String PRINT_SEARCH_WORKER_TIP = "查找到的记录有:"; private static final String SORT_BY_SCORE_TITLE = "按评分排序"; private static final String SORT_BY_SCORE_NO_DATA = "没有记录,无法排序!"; private static final String FILTER_BY_JOB_TYPE_TITLE = "按工种筛选"; private static final String FILTER_BY_JOB_TYPE_NO_DATA = "没有记录,无法筛选!"; // 定义输入的提示词 private static final String INPUT_NAME_TIP = "请输入姓名【exit-返回】:"; private static final String INPUT_JOB_TYPE_TIP = "请输入工种【exit-返回】:"; private static final String INPUT_WORK_YEARS_TIP = "请输入工龄(0-70)【exit-返回】:"; private static final String INPUT_SCORE_TIP = "请输入评分(0-5)【exit-返回】:"; private static final String INPUT_SORT_BY_SCORE = "请选择排序方式(1-升序 |2-降序)【exit-返回】:"; private static final String PREFIX_FILTER_BY_JOB_TYPE_TIP = "请选择工种筛选的序号"; private static final String SUFFIX_FILTER_BY_JOB_TYPE_TIP = "【exit-返回】):"; // 定义其他提示词 private static final String INPUT_IS_BLANK_TIP = "空的输入,请重新输入!"; private static final String INPUT_IS_INVALID_TIP = "无效的输入,请重新输入!"; private static final String MSG_JOB_TYPE_NOT_EXIST = "该工种暂未收录,已记录上报,审核通过后全平台可用"; private static final String ERROR_OUT_OF_RANGE_WORK_YEARS = "工龄不能小于 0 或 大于 70 !"; private static final String ERROR_OUT_OF_RANGE_SCORE = "评分不能小于 0 或 大于 5 !"; private static final String ERROR_OUT_OF_SIZE_WORKERS = "数据已满仓,无法添加!"; private static final String DATA_SAVED_SUCCESSFULLY = "数据已成功保存!"; // 定义正则(主菜单、工龄、评分、工种筛选菜单) private static final String MAIN_MENU_ITEM_NUMBER_REGEX = "[1-6]"; private static final String WORK_YEARS_REGEX = "\\d+"; private static final String SCORE_REGEX = "[0-5](\\.[0-9])?"; private static final String SORT_BY_SCORE_REGEX = "[1-2]"; // 定义哨兵值:-1(exit) -2(isBlank) -3(invalid) private static final int SENTINEL_IS_EXIT = -1; private static final int SENTINEL_IS_INVALID = -3; // 定义用户数组大小、增加用户的计数器(count 是"名册"的状态,不是"师傅"的属性,群体计数是看管数组的那一方的事) private static final int WORKERS_SIZE = 10; private static Worker34[] workers = new Worker34[WORKERS_SIZE]; private static int addWorkerCount = 0; // 模糊查找用户的计数器降级(降级为searchWorker局部变量,每次搜索清零、不跨方法存活,没资格当全局) public static void main(String[] args) { Scanner in = new Scanner(System.in); while (true) { System.out.println(APP_TITLE); System.out.println(APP_LINE.repeat(6)); System.out.println(MENU_TIP); System.out.print(MENU_INPUT_TIP); String inputtedStr = judgeInput(in); if (inputtedStr == null) { System.out.println(EXIT_TIP); break; } int menuItemNumber = getMenuItemNumber(inputtedStr, MAIN_MENU_ITEM_NUMBER_REGEX); switch (menuItemNumber) { case 1 -> { addWorker(in); } case 2 -> { searchWorker(in); } case 3 -> { printAllWorkers(); } case 4 -> { filterByJobType(in); } case 5 -> { sortByScore(in); } case 6 -> { calcAvgWorkYears(); } default -> { } } ; } } /** * 增加师傅 * * @param in 键盘输入 */ public static void addWorker(Scanner in) { // 1. 判断是否满员,是就返回 if (addWorkerCount == WORKERS_SIZE) { System.out.println(ERROR_OUT_OF_SIZE_WORKERS); return; } // 2. 调用方法 String name = inputtedName(in); if (name == null) { return; } String jobType = inputtedJobType(in); if (jobType == null) { return; } int workYears = inputtedWorkYears(in); if (workYears == SENTINEL_IS_EXIT) { return; } double score = inputtedScore(in); if (score == SENTINEL_IS_EXIT) { return; } workers[addWorkerCount] = new Worker34(name, jobType, workYears, score); addWorkerCount++; System.out.println(APP_LINE.repeat(6)); System.out.println(DATA_SAVED_SUCCESSFULLY); System.out.println("当前有:" + addWorkerCount + " 位师傅."); System.out.println(APP_LINE.repeat(6)); System.out.println(workers[addWorkerCount - 1]); } /** * 按姓名模糊查找 * * @param in 键盘输入 */ public static void searchWorker(Scanner in) { int foundCount = 0; // 找到符合的记录计数器 int[] foundIdxs = new int[addWorkerCount]; // 存放符合的记录的下标 System.out.print(APP_LINE.repeat(3)); System.out.print(SEARCH_WORKER_TITLE); System.out.print(APP_LINE.repeat(3) + "\n"); if (addWorkerCount == 0) { // 1. 判断是否有记录,无就提示并返回 System.out.println(SEARCH_WORKER_NO_DATA); } else { // 2. judgeInput() System.out.print(INPUT_NAME_TIP); String inputStr = inputtedName(in); if (inputStr == null) { // 3. 拦截 exit,避免 contains(null) return; } // 4. 获取输入,String.contains()或String.indexOf() for (int i = 0; i < addWorkerCount; i++) { if (workers[i].getName().contains(inputStr)) { // 5. 有符合的记录就保存下标,并继续查找 foundIdxs[foundCount] = i; foundCount++; } } // 4.1 查找到符合记录的计数器 System.out.print(PRINT_SEARCH_WORKER_TIP + foundCount + " 条.\n"); if (foundCount > 0) { for (int j = 0; j < foundCount; j++) { // 4.2 根据保存的下标,打印记录 System.out.println("[第 " + (j + 1) + " 条记录] " + workers[foundIdxs[j]]); } } } } /** * 打印全部师傅的信息,分为无记录提示,有记录打印 */ public static void printAllWorkers() { System.out.print(APP_LINE.repeat(3)); System.out.print(PRINT_ALL_WORKERS_TITLE); System.out.print(APP_LINE.repeat(3) + "\n"); if (addWorkerCount == 0) { // 没有记录 System.out.println(PRINT_NO_DATA); } else { // 有记录 for (int i = 0; i < addWorkerCount; i++) { System.out.println("[第 " + (i + 1) + " 条记录] " + workers[i]); } } } public static void filterByJobType(Scanner in) { int jobTypeCount = 0; // 不同工种的计数器 String[] jobTypeData = new String[addWorkerCount]; // 存放不同工种的数据 StringBuilder menu = new StringBuilder(); // 不同工种拼接 while (true) { System.out.print(APP_LINE.repeat(3)); System.out.print(FILTER_BY_JOB_TYPE_TITLE); System.out.println(APP_LINE.repeat(3)); // 1. 判断是否有记录,无就提示并返回 if (addWorkerCount == 0) { System.out.println(FILTER_BY_JOB_TYPE_NO_DATA); break; } // 2. 获取源记录中工种的种类,并生成菜单,拼接前缀 + 工种 + 后缀,以便用户选择 if (addWorkerCount == 1) { System.out.println("有 " + addWorkerCount + " 条记录"); System.out.println(workers[0]); break; } if (addWorkerCount > 1) { for (int i = 0; i < addWorkerCount; i++) { String type = workers[i].getJobType(); boolean isExist = false; // 检查jobTypeData[]是否存在 for (int j = 0; j < jobTypeCount; j++) { // 如果相同就退出 if (type.equalsIgnoreCase(jobTypeData[j])) { isExist = true; break; } } // 不相同就写入 if (!isExist) { jobTypeData[jobTypeCount++] = type; } } // 去重工种的数组生成菜单字符串 menu.setLength(0); for (int i = 0; i < jobTypeCount; i++) { menu.append(i + 1).append(" - ").append(jobTypeData[i]).append(" |"); } String menuStr = menu.toString(); System.out.print(PREFIX_FILTER_BY_JOB_TYPE_TIP + "(" + menuStr + SUFFIX_FILTER_BY_JOB_TYPE_TIP); String inputStr = judgeInput(in); // 预处理键盘输入 if (inputStr == null) { break; } // 生成菜单正则 String menuRegex = "[1-" + jobTypeCount + "]"; int menuItemNumber = getMenuItemNumber(inputStr, menuRegex); if ((menuItemNumber > jobTypeCount) || (menuItemNumber < 1)) { System.out.println(INPUT_IS_INVALID_TIP); continue; } // 按照选择的工种,遍历查找,找到就保存下标,继续查找 int[] foundIdxs = new int[addWorkerCount]; int foundCount = 0; for (int i = 0; i < addWorkerCount; i++) { if (workers[i].getJobType().equalsIgnoreCase(jobTypeData[menuItemNumber - 1])) { foundIdxs[foundCount++] = i; } } System.out.println("筛选到 " + foundCount + " 条数据"); for (int i = 0; i < foundCount; i++) { System.out.println(workers[foundIdxs[i]]); } break; } } } public static void sortByScore(Scanner in) { while (true) { System.out.print(APP_LINE.repeat(3)); System.out.print(SORT_BY_SCORE_TITLE); System.out.print(APP_LINE.repeat(3)); if (addWorkerCount == 0) { System.out.println(SORT_BY_SCORE_NO_DATA); break; } else { System.out.print("\n" + INPUT_SORT_BY_SCORE); String inputStr = judgeInput(in); // 1. 用户选择升序还是降序 if (inputStr == null) { return; } doByScore(inputStr); // 2. 按用户选择排序 break; } } } public static void doByScore(String sortOrderStr) { if (!sortOrderStr.matches(SORT_BY_SCORE_REGEX)) { System.out.println(INPUT_IS_INVALID_TIP); return; } int sortOrderNum = Integer.parseInt(sortOrderStr); if (sortOrderNum == 1) { // 升序 for (int i = 0; i < addWorkerCount - 1; i++) { for (int j = 0; j < addWorkerCount - 1 - i; j++) { boolean needSwap = (sortOrderNum == 1) ? workers[j].getScore() > workers[j + 1].getScore() // 升序:左>右才换 : workers[j].getScore() < workers[j + 1].getScore(); // 降序:左<右才换 if (needSwap) { Worker34 t = workers[j]; workers[j] = workers[j + 1]; workers[j + 1] = t; } } } } if (sortOrderNum == 2) { // 降序 for (int i = 0; i < addWorkerCount - 1; i++) { for (int j = 0; j < addWorkerCount - 1 - i; j++) { boolean needSwap = (sortOrderNum == 1) ? workers[j].getScore() > workers[j + 1].getScore() // 升序:左>右才换 : workers[j].getScore() < workers[j + 1].getScore(); // 降序:左<右才换 if (needSwap) { Worker34 t = workers[j]; workers[j] = workers[j + 1]; workers[j + 1] = t; } } } } // 排序完打印 printAllWorkers(); } /** * 计算全部师傅的平均工龄 */ public static void calcAvgWorkYears() { if (addWorkerCount == 0) { // 1. 判断是否有记录,无就提示并返回 System.out.println(CALCULATE_AVERAGE_WORK_YEARS_NO_DATA); } else { System.out.print(APP_LINE.repeat(3)); System.out.print(CALCULATE_AVERAGE_WORK_YEARS_TITLE); System.out.print(APP_LINE.repeat(3) + "\n"); double avgWorkYears = 0.0; // 2. 遍历计算平均值并返回 for (int i = 0; i < addWorkerCount; i++) { avgWorkYears += workers[i].getWorkYear(); } avgWorkYears = Math.round((avgWorkYears / addWorkerCount) * 10) / 10; // 3. 获取师傅的数量,计算平均值 System.out.println("当前有:" + addWorkerCount + " 位师傅."); System.out.println(PRINT_AVERAGE_WORK_YEARS_TIP + avgWorkYears + "年"); } } /** * 获取姓名 * * @param in 键盘输入 * @return 返回字符串 */ public static String inputtedName(Scanner in) { String inputtedStr; while (true) { System.out.print(INPUT_NAME_TIP); inputtedStr = judgeInput(in); if (inputtedStr == null) { break; } if (inputtedStr.isBlank()) { continue; } break; } return inputtedStr; } /** * 获取工种 * * @param in 键盘输入 * @return 返回字符串 */ public static String inputtedJobType(Scanner in) { String inputtedStr; while (true) { System.out.print(INPUT_JOB_TYPE_TIP); inputtedStr = judgeInput(in); if (inputtedStr == null) { break; } if (inputtedStr.isBlank()) { System.out.println(INPUT_IS_BLANK_TIP); continue; } // 2. 获取输入,判断是否在字典里,不在就提示并重新输入 boolean isHit = false; for (String type : JOB_TYPE_DICT) { if (type.equalsIgnoreCase(inputtedStr)) { isHit = true; break; } } if (!isHit) { System.out.println(MSG_JOB_TYPE_NOT_EXIST); } else { return inputtedStr; } } return null; } /** * 获取工龄 * * @param in 键盘输入 * @return int,0-70 */ public static int inputtedWorkYears(Scanner in) { int inputtedNum = 0; while (true) { System.out.print(INPUT_WORK_YEARS_TIP); String inputStr = judgeInput(in); // 1. judgeInput() if (inputStr == null) { return SENTINEL_IS_EXIT; } if (inputStr.isBlank()) { System.out.println(INPUT_IS_BLANK_TIP); continue; } if (!inputStr.matches(WORK_YEARS_REGEX)) { // 2. 获取输入,正则(INT_WORK_YEARS_REGEX)判断,不符合就提示并重新输入 System.out.println(INPUT_IS_INVALID_TIP); } else { inputtedNum = Integer.parseInt(inputStr); if (inputtedNum < 0 || inputtedNum > 70) { // 3. 判断,小于0拦,大于70拦(0<= workYears <= 70放行) System.out.println(ERROR_OUT_OF_RANGE_WORK_YEARS); continue; } return inputtedNum; } } } /** * 获取评分 * * @param in 键盘输入 * @return double类型 0-5 */ public static double inputtedScore(Scanner in) { double inputtedNum = 0.0; while (true) { System.out.print(INPUT_SCORE_TIP); String inputStr = judgeInput(in); if (inputStr == null) { return SENTINEL_IS_EXIT; } if (inputStr.isBlank()) { System.out.println(INPUT_IS_BLANK_TIP); continue; } if (!inputStr.matches(SCORE_REGEX)) { // 2. 获取输入,正则(SCORE_REGEX)判断,不符合就提示并重新输入 System.out.println(INPUT_IS_INVALID_TIP); } else { inputtedNum = Double.parseDouble(inputStr); if (inputtedNum < 0 || inputtedNum > 5) { // 3. 判断,小于0拦,大于5拦(0<= score <= 5放行) System.out.println(ERROR_OUT_OF_RANGE_SCORE); continue; } return inputtedNum; } } } /** * 获取菜单编号,只接收1-6和 exit,其他无效 * * @param inputStr 调用方法 judgeInput()预处理后返回的字符串 * @param regex 菜单正则 * @return 返回菜单编号 */ public static int getMenuItemNumber(String inputStr, String regex) { // 1. judgeInput() // 2. 获取输入,正则判断,不符合就提示并重新输入 if (!inputStr.matches(regex)) { System.out.println(INPUT_IS_INVALID_TIP); return SENTINEL_IS_INVALID; } // 3. 返回菜单编号 return Integer.parseInt(inputStr); } /** * 预处理键盘输入,各个方法抽取出来的,不抢占原来方法的功能 * * @param in 键盘输入 * @return ”“、null、原串 */ public static String judgeInput(Scanner in) { String inputStr = in.nextLine().strip(); if (inputStr.isBlank()) { return ""; } if (EXIT_KEY_WORD.equalsIgnoreCase(inputStr)) { return null; } return inputStr; } } ``` ```/** * * day29:定义Worker类(姓名/工种/工龄/评分),创建2个师傅对象并打印信息。 * day30:给Worker加3种构造:无参/姓名+工种/全字段,分别创建对象验证;用this区分同名参数 * day31:给Worker加static计数器count,每创建一个对象+1,打印当前平台注册师傅总数 * day32:Worker字段全改private;setWorkYears里校验工龄不能<0(负数就置0或抛提示),setScore校验0-5分 * day33:师傅管理小程序:Worker数组容量10,支持添加、按名字查找、打印全部(遍历调用toString前先自己拼信息) * day34:在Day5基础上加:按工种筛选、按评分排序、统计平均工龄,全部封装成方法 * day35:默写Worker类(封装+构造器+计数器版) * */ class Worker34 { private String name; private String jobType; private int workYear; private double score; public Worker34(){this(null,null,0,0.0);} public Worker34(String name, String jobType){ this(name, jobType, 0, 0.0); } public Worker34(String name, String jobType, int workYear, double score){ this.setName(name); this.setJobType(jobType); this.setWorkYear(workYear); this.setScore(score); } public void setName(String name){ this.name = name; } public void setJobType(String jobType){ this.jobType = jobType; } public void setWorkYear(int workYear){ if (workYear < 0){ System.out.println("工龄不能小于0,已置0"); this.workYear = 0; }else{ this.workYear = workYear; } } public void setScore(double score){ if (!(score >= 0 && score <=5)){ System.out.println("评分不能小于0或大于5,已置0"); this.score = 0; }else{ this.score = score; } } public String getName(){ return this.name; } public String getJobType(){ return this.jobType; } public int getWorkYear(){ return this.workYear; } public double getScore(){ return this.score; } @Override public String toString(){ return ("姓名:" + this.getName() + ",工种:" + this.getJobType() + ",工龄:" + this.getWorkYear() + ",评分:" + this.getScore()); } } ```
来一个企业级思维骨架
```import java.util.Scanner; /** * day29:定义Worker类(姓名/工种/工龄/评分),创建2个师傅对象并打印信息。 * day30:给Worker加3种构造:无参/姓名+工种/全字段,分别创建对象验证;用this区分同名参数 * day31:给Worker加static计数器count,每创建一个对象+1,打印当前平台注册师傅总数 * day32:Worker字段全改private;setWorkYears里校验工龄不能<0(负数就置0或抛提示),setScore校验0-5分 * day33:师傅管理小程序:Worker数组容量10,支持添加、按名字查找、打印全部(遍历调用toString前先自己拼信息) * day34:在day33基础上加:按工种筛选、按评分排序、统计平均工龄,全部封装成方法 * day35:默写Worker类(封装+构造器+计数器版) * * * 菜单:1-增加师傅 |2-按名字模糊查找 |3-打印全部 |4-按工种筛选 |5-按评分排序 |6-统计平均工龄 |exit-退出 * 方法: * 1. addWorker() * 2. searchWorker() * 3. printAllWorkers() * 4. filterByJobType() * 5. sortByScore() * 6. calcAvgWorkYears() * 7. inputtedName() * 8. inputtedJobType() * 9. inputtedWorkYears() * 10. inputtedScore() * 11. getMenuItemNumber() * 12. judgeInput() */ public class Day34WorkerTest{ // 定义程序、菜单、工种字典 // 定义输入的提示词 // 定义其他提示词 // 定义正则(主菜单、工龄、评分、工种筛选菜单) // 定义哨兵值:-1(exit) -2(isBlank) -3(invalid) // 定义用户数组大小、增加用户的计数器(count 是"名册"的状态,不是"师傅"的属性,群体计数是看管数组的那一方的事)、 // 模糊查找用户的计数器降级(降级为searchWorker局部变量,每次搜索清零、不跨方法存活,没资格当全局) public static void main(String[] args){ // 菜单流程:1-增加师傅 |2-按名字模糊查找 |3-打印全部 |4-按工种筛选 |5-按评分排序 |6-统计平均工龄 |exit-退出 // 键盘输入 Scanner in = new Scanner(System.in); while(true){ // 程序相关信息展示 // judgeInput() switch(selectedItemNumber) { case 1 -> {addWorker(in);} case 2 -> {searchWorker(in);} case 3 -> {printAllWorders();} case 4 -> {filterByJobType(in);} case 5 -> {sortByScore(in);} case 6 -> {calcAvgWorkYears();} default -> {} }; } } /* 方法 */ // addWorker() // 1. 判断是否满员,是就返回 // 2. 调用方法 // inputtedName() // inputtedJobType() // inputtedWorkYears() // inputtedScore() // searchWorker() // 1. 判断是否有记录,无就提示并返回 // 2. judgeInput() // 3. 获取输入,String.contains()或String.indexOf() // 4. 有符合的记录就保存下标,并继续查找 // 4.1 查找到符合记录的计数器 // 4.2 根据保存的下标,打印记录 // printAllWorkers() // 1. 获取可打印数据的数量 // 1.1 无就提示 0 条记录,并返回 // 1.2 遍历打印 // filterByJobType() // 1. 判断是否有记录,无就提示并返回 // 2. judgeInput() // 3. 获取源记录中工种的种类,并生成菜单,以便用户选择 // 4. 根据用户的选择跳转 // sortByScore() // 1. 用户选择升序还是降序 // 2. 按用户选择排序 // calcAvgWorkYears() // 1. 判断是否有记录,无就提示并返回 // 2. 获取师傅的数量 // 3. 遍历计算平均值并返回 // inputtedName() // 1. judgeInput() // 2. 获取输入并保存 // inputtedJobType() // 1. judgeInput() // 2. 获取输入,判断是否在字典里,不在就提示并重新输入 // 3. 保存 // inputtedWorkYears() // 1. judgeInput() // 2. 获取输入,正则(INT_WORK_YEARS_REGEX)判断,不符合就提示并重新输入 // 3. 判断,小于0拦,大于70拦(0<= workYears <= 70放行) // 4. 保存 // inputtedScore() // 1. judgeInput() // 2. 获取输入,正则(DOUBLE_SCORE_REGEX)判断,不符合就提示并重新输入 // 3. 判断是否大于0与小于等于5(0放行),否就提示并重新输入 // 4. 保存 // getMenuItemNumber() // 1. judgeInput() // 2. 获取输入,正则(MAIN_MENU_ITEM_NUMBER_REGEX/FILTER_JOBTYPE_MENU_ITEM_NUMBER_REGEX)判断,不符合就提示并重新输入 // 3. 返回菜单编号 // judgeInput() // 1. 判断有无输入,无就提示重新输入 // 2. 判断输入是否为空,是就提示重新输入 // 3. 判断是否输入"exit",是就返回null // 4. 获取输入的数据 } ```
先有企业级代码思维(代码写健壮),再有企业级代码
```/** * day29:定义Worker类(姓名/工种/工龄/评分),创建2个师傅对象并打印信息。 * day30:给Worker加3种构造:无参/姓名+工种/全字段,分别创建对象验证;用this区分同名参数 * day31:给Worker加static计数器count,每创建一个对象+1,打印当前平台注册师傅总数 * day32:Worker字段全改private;setWorkYears里校验工龄不能<0(负数就置0或抛提示),setScore校验0-5分 * day33:师傅管理小程序:Worker数组容量10,支持添加、按名字查找、打印全部(遍历调用toString前先自己拼信息) * day34:在Day5基础上加:按工种筛选、按评分排序、统计平均工龄,全部封装成方法 * day35:默写Worker类(封装+构造器+计数器版) * * */ import java.util.Scanner; public class Day33WorkerTest { // 定义程序、输入等的提示词 private static final int MENU_ITEM_NUMBER_ADD = 1; private static final int MENU_ITEM_NUMBER_SEARCH = 2; private static final int MENU_ITEM_NUMBER_PRINT = 3; private static final String APP_EXIT_WORD = "EXIT"; private static final String APP_MENU = "【1-增加师傅 | 2-按名字模糊查找 | 3-打印全部 | exit-退出】"; private static final String INPUT_MENU_TIP = "请输入您的选择:"; private static final String[] JOB_TYPE_DICT = {"水电工", "木工", "瓦工", "油漆工", "空调工", "智能调试"}; private static final String APP_TITLE = "师傅管理小程序"; private static final String APP_LINE = "--------------------------------------------------------------------------------"; // 定义提示词常量 private static final String EXIT_TIP = "Thanks. See you next time."; private static final String APP_MENU_NAME_TIP = "请输入姓名【exit - 退出】:"; private static final String APP_MENU_JOB_TYPE_TIP = "请输入工种【exit - 退出】:"; private static final String APP_MENU_WORK_YEAR_TIP = "请输入工龄(不能小于0,不能大于70)【exit - 返回】:"; private static final String APP_MENU_SCORE_TIP = "请输入评分(0-5分,可以小数[小数点后一位])【exit - 返回】:"; private static final String ERROR_INPUT_IS_BLANK_TIP = "空输入,请重新输入!"; private static final String ERROR_INPUT_INVALID_TIP = "无效的输入,请重新输入!"; private static final String OVER_MAX_SIZE_TIP = "名额已满,无法再增加!"; public static final String MSG_WORK_TYPE_NOT_EXIST = "该工种暂未收录,已为你记录上报,审核通过后全平台可用"; public static final String NO_DATA_FOR_FOUND_TIP = "没有可供查找的数据,请先输入师傅的数据!"; public static final String NO_DATA_FOUND_TIP = "没有找到数据!"; public static final String NO_DATA_FOR_PRINT_TIP = "没有数据可供打印,请先输入数据!"; // 定义哨兵值常量 private static final int SENTINEL_EXIT = -1; private static final int SENTINEL_IS_BLANK = -2; private static final int SENTINEL_IS_INVALID = -3; // 正则表达式,菜单(1-3)、工龄(大于0)、评分(0-5分,可以一位小数) private static final String MENU_ITEM_NUMBER_REGEX = "[1-3]"; // 菜单 private static final String WORK_YEAR_REGEX = "[0-9]+"; // 拦截负数,0放行 private static final String SCORE_REGEX = "[0-9]+(\\.[0-9])?"; // 非负整数或一位小数 // 定义用户数组的容量、声明数组、用户增加计数器 private static final int WORKER_SIZE = 10; private static Worker33[] workers = new Worker33[WORKER_SIZE]; private static int foundCount = 0; // 定义查找到符合记录的计数器 public static void main(String[] args) { Scanner in = new Scanner(System.in); // 菜单流程 while (true) { System.out.println(APP_TITLE); System.out.println(APP_LINE); System.out.println(APP_MENU); System.out.print(INPUT_MENU_TIP); int inputMenuItemNumber = getMenuItemNumber(in.nextLine().strip()); if (inputMenuItemNumber == SENTINEL_EXIT) { System.out.println(EXIT_TIP); break; } switch (inputMenuItemNumber) { case MENU_ITEM_NUMBER_ADD -> { addWorker(in); } case MENU_ITEM_NUMBER_SEARCH -> { searchWorker(in); } case MENU_ITEM_NUMBER_PRINT -> { printAllWorkerInfo(); } default -> { System.out.println(ERROR_INPUT_INVALID_TIP); } } } } /** * 模糊搜索,可以搜索多条符合的记录 * * @param in 键盘输入 */ public static void searchWorker(Scanner in) { int sourceRecord = Worker33.count; // 源记录有多少条(有多少条数据可供查找) foundCount = 0; // 查找统计计数器清零 // 没有源记录 if (sourceRecord == 0) { System.out.println(NO_DATA_FOR_FOUND_TIP); return; } // 有源记录 String searchName = inputName(in); if (searchName == null) { return; } // 源记录有 1 条及以上 int[] foundIdxs = new int[sourceRecord]; // 创建源记录数量的数组 for (int i = 0; i < sourceRecord; i++) { // 空数据不查找 String name = workers[i].getName(); int foundIdx = name.indexOf(searchName); if (foundIdx != -1) { foundIdxs[foundCount] = i; foundCount++; } } if (foundCount>0){ for (int j = 0; j < foundCount; j++) { System.out.println(workers[foundIdxs[j]]); } }else{ System.out.println(NO_DATA_FOUND_TIP); } } /** * 打印全部记录 */ public static void printAllWorkerInfo() { int sourceRecord; // 源记录有多少条(有多少条数据可供查找) sourceRecord = Worker33.count; // 没有源记录 if (sourceRecord == 0) { System.out.println(NO_DATA_FOR_PRINT_TIP); } else { for (int i = 0; i < sourceRecord; i++) { // 有数据才打印,空数据不打印 System.out.println(workers[i]); } } } /** * 输入姓名 * * @param in 键盘输入 * @return */ public static String inputName(Scanner in) { while (true) { System.out.print(APP_MENU_NAME_TIP); String inputStr = in.nextLine().strip(); // 空输入 if (inputStr.isBlank()) { System.out.println(ERROR_INPUT_IS_BLANK_TIP); } else { if (APP_EXIT_WORD.equalsIgnoreCase(inputStr)) { return null; } return inputStr; } } } /** * 输入工种 * * @param in 键盘输入 * @return */ public static String inputJobType(Scanner in) { while (true) { System.out.print(APP_MENU_JOB_TYPE_TIP); String inputStr = in.nextLine().strip(); // 空输入 if (inputStr.isBlank()) { System.out.println(ERROR_INPUT_IS_BLANK_TIP); continue; } else { if (APP_EXIT_WORD.equalsIgnoreCase(inputStr)) { return null; } boolean isHit = false; for (String type : JOB_TYPE_DICT) { if (type.equalsIgnoreCase(inputStr)) { isHit = true; break; } } if (!isHit) { System.out.println(MSG_WORK_TYPE_NOT_EXIST); continue; } return inputStr; } } } /** * 输入工龄 * * @param in 键盘输入 * @return */ public static Integer inputWorkYear(Scanner in) { while (true) { System.out.print(APP_MENU_WORK_YEAR_TIP); String inputStr = in.nextLine().strip(); // 空输入 if (inputStr.isBlank()) { System.out.println(ERROR_INPUT_IS_BLANK_TIP); continue; } if (APP_EXIT_WORD.equalsIgnoreCase(inputStr)) { return null; } if (!inputStr.matches(WORK_YEAR_REGEX)) { System.out.println(ERROR_INPUT_INVALID_TIP); continue; } if (Integer.parseInt(inputStr) > 70) { System.out.println(ERROR_INPUT_INVALID_TIP); continue; } return Integer.valueOf(inputStr); } } /** * 输入评分 * * @param in 键盘输入 * @return */ public static Double inputScore(Scanner in) { while (true) { System.out.print(APP_MENU_SCORE_TIP); String inputStr = in.nextLine().strip(); // 空输入 if (inputStr.isBlank()) { System.out.println(ERROR_INPUT_IS_BLANK_TIP); continue; } if (APP_EXIT_WORD.equalsIgnoreCase(inputStr)) { return null; } if (!inputStr.matches(SCORE_REGEX)) { System.out.println(ERROR_INPUT_INVALID_TIP); continue; } if (Double.parseDouble(inputStr) > 5) { System.out.println(ERROR_INPUT_INVALID_TIP); continue; } return Double.valueOf(inputStr); } } /** * 增加师傅 * * @param in 键盘输入 */ public static void addWorker(Scanner in) { if (Worker33.count >= WORKER_SIZE) { System.out.println(OVER_MAX_SIZE_TIP); return; } String inputtedName = inputName(in); if (inputtedName == null) { return; } String inputtedJobType = inputJobType(in); if (inputtedJobType == null) { return; } Integer inputtedWorkYear = inputWorkYear(in); if (inputtedWorkYear == null) { return; } Double inputedScore = inputScore(in); if (inputedScore == null) { return; } workers[Worker33.count] = new Worker33(inputtedName, inputtedJobType, inputtedWorkYear, inputedScore); System.out.println("【当前共有:" + Worker33.count + " 位师傅】"); } /** * 获取菜单编号 * * @param menuSelected 键盘输入 * @return 返回-1(exit-退出),返回-2(空输入),返回-3(无效输入),正常返回 */ public static int getMenuItemNumber(String menuSelected) { String numStr = menuSelected.strip(); if (numStr.isBlank()) { return SENTINEL_IS_BLANK; } if (APP_EXIT_WORD.equalsIgnoreCase(numStr)) { return SENTINEL_EXIT; } if (!numStr.matches(MENU_ITEM_NUMBER_REGEX)) { return SENTINEL_IS_INVALID; } return Integer.parseInt(numStr); } } ``` ```/** * * day29:定义Worker类(姓名/工种/工龄/评分),创建2个师傅对象并打印信息。 * day30:给Worker加3种构造:无参/姓名+工种/全字段,分别创建对象验证;用this区分同名参数 * day31:给Worker加static计数器count,每创建一个对象+1,打印当前平台注册师傅总数 * day32:Worker字段全改private;setWorkYears里校验工龄不能<0(负数就置0或抛提示),setScore校验0-5分 * day33:师傅管理小程序:Worker数组容量10,支持添加、按名字查找、打印全部(遍历调用toString前先自己拼信息) * day34:在Day5基础上加:按工种筛选、按评分排序、统计平均工龄,全部封装成方法 * day35:默写Worker类(封装+构造器+计数器版) * */ class Worker33 { private String name; private String jobType; private int workYear; private double score; static int count = 0; public Worker33(){this(null,null,0,0.0);} public Worker33(String name, String jobType){ this(name, jobType, 0, 0.0); } public Worker33(String name, String jobType, int workYear, double score){ count++; this.setName(name); this.setJobType(jobType); this.setWorkYear(workYear); this.setScore(score); } public void setName(String name){ this.name = name; } public void setJobType(String jobType){ this.jobType = jobType; } public void setWorkYear(int workYear){ if (workYear < 0){ System.out.println("工龄不能小于0,已置0"); this.workYear = 0; }else{ this.workYear = workYear; } } public void setScore(double score){ if (!(score >= 0 && score <=5)){ System.out.println("评分不能小于0或大于5,已置0"); this.score = 0; }else{ this.score = score; } } public String getName(){ return this.name; } public String getJobType(){ return this.jobType; } public int getWorkYear(){ return this.workYear; } public double getScore(){ return this.score; } @Override public String toString(){ return ("姓名:" + this.getName() + ",工种:" + this.getJobType() + ",工龄:" + this.getWorkYear() + ",评分:" + this.getScore()); } } ```
一条告警的 10 秒:从 -5 dB 到"已处理"
12:31:46,一条 `snr = -5` 的遥测记录落库。 几秒之内,屏幕右下角亮起红色弹窗:**严重告警 · 下行信噪比过低 · Starlink-1002 · snr=-5.00 below threshold 0.00**,顶栏铃铛的未读角标跳到 1。12:31:51,值班的人在告警中心点开详情、按下"确认处置"——确认人 admin、确认时间落库。又过一个评估周期,一条 `snr = 25` 的正常数据写入,这条记录自动翻成绿色:**已恢复**。  不是演示动画。上面的时间戳全部来自交付验收时真实跑出来的链路。这篇工程化笔记(系列第四篇)就按这"十秒"的先后顺序,把系统主动喊人的整条链路拆开:规则怎么定义、评估引擎在轮询什么、通知怎么送到浏览器、人点了确认之后状态机怎么走。最后两站,是这次一起交付的两个扩展——**遥测健康评估升级(SU4)** 与多数据源的**数据管理中心**。 ## 第 1 站 · 规则:判决书要提前写好 评估引擎里没有一行写死的判定逻辑。什么算"越限",全部来自规则表——一条规则回答四个问题: - **盯谁**:指定卫星,或全体卫星(`satellite_id` 为空); - **盯什么**:8 个遥测指标之一(电量、舱温、信噪比、存储占用……); - **什么算越限**:四类规则任选; - **多严重**:INFO / WARNING / CRITICAL 三档。 四类规则各管一种"越限"语义: | 类型 | 语义 | 一句话例子 | | --- | --- | --- | | THRESHOLD | 单阈值比较(GT / LT) | 电量 **低于 20%** | | RANGE | 区间判定(OUTSIDE / INSIDE) | 舱温**跑出 [0, 60]℃** | | COMPOSITE | 多参数联合判读(AND / OR,2~5 条条件) | **温度高 且 功耗高**才算数 | | TREND | 趋势斜率比较(带符号) | 数值没越线,但**掉得比 0.5/分钟还快** | 后两类是本次新扩展的判读能力,放在第 5 站展开。这里先说两个规则引擎的设计决策。 **第一条:规则的"法条"钉在数据库里。** 应用层的校验总有可能被绕过(迁移脚本、手工改库、未来新写的接口),所以我们把语义同时写成了库层 CHECK 约束——RANGE 必须满足下界 < 上界、COMPOSITE 的条件数组必须是 2~5 个元素、TREND 的回看窗口必须在 30 秒~24 小时之间。任何一条不合法的规则,无论从哪个入口进来,都会被数据库当场拒绝: ```sql -- V6__alert_su4.sql(节选示意,省略部分互斥判据) ALTER TABLE alert_rule ADD CONSTRAINT alert_rule_threshold_check CHECK ( (rule_type = 'THRESHOLD' AND operator IN ('GT', 'LT') AND threshold IS NOT NULL) OR (rule_type = 'RANGE' AND threshold_min IS NOT NULL AND threshold_max IS NOT NULL AND threshold_min < threshold_max) OR (rule_type = 'COMPOSITE' AND condition_json IS NOT NULL AND CASE WHEN jsonb_typeof(condition_json) = 'array' THEN jsonb_array_length(condition_json) BETWEEN 2 AND 5 ELSE FALSE END) OR (rule_type = 'TREND' AND slope_threshold IS NOT NULL AND window_seconds IS NOT NULL AND window_seconds BETWEEN 30 AND 86400) ); ``` 一个值得注意的细节:PostgreSQL 不保证 OR 链的求值顺序,直接对非数组的 JSON 调 `jsonb_array_length` 可能当场报错——所以长度校验用 `CASE WHEN jsonb_typeof(...) = 'array'` 先验类型、再量长度。约束里的每一行,都是被生产环境教育出来的。 **第二条:指标白名单只有一份来源。** 规则校验要用指标集合、评估引擎要按指标从遥测快照里取值、判读策略还要复用同一套比较语义——这三处如果各写一份,早晚对不上。我们建了一个 `AlertMetrics` 注册表:指标名 → 遥测列取值函数的映射,校验、取值、比较全部共用它。以后加新指标,只在这个注册表里登记一行。 另外两个细节:服务层会做**字段归一化**——比如把 THRESHOLD 规则里残留的 min/max 字段清空,保证"库里每一行都能被 CHECK 约束解释";告警记录会**快照**规则名、卫星名、指标、级别——所以规则改名、删除、重建,历史告警永远自解释。(截图里的演示规则在出图后已删除,记录仍然完整可读,就是这条设计的现实版演示。) ## 第 2 站 · 评估:每 10 秒一次的"全量体检" 这条链路的核心不是"事件驱动"——没有人在遥测写入时同步判断。评估引擎是一个每 10 秒出发一次的巡官(`fixedDelay`,启动延迟 15 秒,先让数据库迁移和分区维护跑完),每一轮把全平台的"规则 × 卫星"组合过一遍。 一轮的内部流程: 1. 加载全部启用规则(固定 id 升序,日志顺序稳定); 2. 汇总规则实际涉及的卫星,**只对它们**取最新遥测快照——每颗卫星一次 `LIMIT 1` 索引点查,查询代价不随遥测表分区增长; 3. 逐组合判定,三态分流: - **BREACHED(越限)**:进入触发流程; - **WITHIN(正常)**:如果有未恢复的记录,自动转"已恢复"; - **SKIP(无法判定)**:指标列缺失时**不触发、也不恢复**——保持现状。 三态里的 SKIP 值得多解释一句。"指标缺失"最容易写错的地方是把它当成"恢复正常"——那样一条 FIRING 记录会因为一次数据缺列被误报"已恢复"。我们的选择是:**无法判定就什么都不做**,宁可等下一轮。 **持续时长规则**("存储占用高于 90% 持续 5 分钟"这类)需要跨轮次记住"第一次越限是什么时候"。我们用一个 Redis 键(`leo:alert:breach:{规则ID}:{卫星ID}`)存首次越限时间戳,TTL 设为"持续时间 + 10 分钟缓冲"、期间每轮续期;满时长才触发,恢复正常立刻删键、重新计时。 Redis 挂了呢?这里有一个反直觉的取舍:**降级为"本轮不触发",而不是"立即触发"**。把"计时失败"当成"时间已到"会制造大量误报;而漏掉一轮、Redis 恢复后重新计时,只损失几分钟的延迟。这类降级还做了日志限流——故障时记一条 WARN、恢复时记一条 INFO,不刷屏。 **去重**是老话题但有硬细节:命中 `(rule_id, satellite_id, status)` 上的专用索引,同一"规则 × 卫星"只要存在未恢复记录(触发中或已确认),就**原地刷新**指标值和消息,不新建、不重复推送。指标持续恶化时,列表里永远只有一条记录,值在不断更新。(算一笔账:没有这条去重,10 秒一轮、故障持续一小时,就是 360 条重复记录。) **自动恢复**:最新值回到阈值内,FIRING 和 ACKNOWLEDGED 记录一律转"已恢复",恢复时间落库;但**不会抹掉确认人和确认时间**——"这条告警当初是谁处置的",永远查得到。 再补一张安全的网——**单点隔离**贯穿全轮:某颗卫星的快照加载失败,只跳过这颗卫星;某个组合判定抛异常,记录后继续下一个组合;推送失败,不影响落库。10 秒一班的巡官不会因为一个坏样本停摆。  ### 这一站顺带讲一个"只有真实库才能暴露的 Bug" V5 迁移给告警记录加了"已读"列,`NOT NULL` + 数据库默认值 `false`。逻辑听起来万无一失,但真实链路上线后,**告警一条都落不进库**。 根因:MyBatis-Flex 的 insert 会把实体里没赋值的字段**显式写成 NULL**——而"显式 NULL"会覆盖掉数据库的 `DEFAULT false`,直接撞上 `NOT NULL` 约束。数据没问题、SQL 没问题,是"默认值"这个承诺被 ORM 悄悄撕掉了。 为什么之前没抓到?两层巧合叠加:单测全程 mock,验证不到库层约束;而 D27 之前,真实数据库里从没有过一条被触发的告警(演示都是 mock 数据)。直到这次用"真后端 + 真 WS + 真落库"的端到端用例才把它钓出来。修复只有一行(新建记录时显式置"未读"),但补了回归断言。 两句话总结这个坑:**数据库默认值在 ORM 的显式 NULL 面前是纸糊的**;以及——**有些缺陷只活在真实链路里,mock 测试永远绿得心安理得,却什么都证明不了**。 ## 第 3 站 · 送达:从 STOMP 广播到铃铛亮起 评估引擎发现越限后,把事件交给"通知通道"。这里用了一个可插拔的接口 `AlertNotifier`:`alertFired` / `alertResolved` 两个事件,当前唯一实现是把消息广播到 STOMP 主题 `/topic/alerts` 的 WebSocket 通道;将来要加邮件通道,实现接口、注册 Bean 即可,评估引擎零改动。单个通道推送失败只记日志,不影响落库和其他通道。 推送载荷是一个 12 字段的触发快照(事件类型、记录 ID、规则与卫星名、指标与值、级别、消息、时间戳)——前端拿到就能直接渲染,不需要再回查详情。这条通道的语义是"**尽力而为**":没有在线订阅者就直接丢弃,因为记录永远在库里、列表永远查得到。实时是体验,落库才是事实。 前端这一侧,是这次通知中心(D27)的全部工作: - **一条长连接**:SockJS + STOMP 客户端,`CONNECT` 帧带 JWT(复用此前已完成鉴权的 STOMP 通道),断线 5 秒自动重连、双向 10 秒心跳;连接状态三态(连接中/已连接/已断开)暴露给 UI——面板底部的绿点"实时推送已连接"就是它(见图)。 - **未读计数双轨**:收到 ALERT_FIRING 事件本地 +1(实时),打开面板和进入页面时从后端拉取校正(兜底)。实时链路允许丢消息,所以任何时刻都要有一条"从库里拉"的退路。 - **弹窗策略**:只有 CRITICAL 级别弹窗(红色通知、10 秒自动消退、点击跳转告警中心),WARNING / INFO 只进列表——弹窗是稀缺资源,滥弹一次,以后就没人看了。并且加了**同源去重**:同一"规则 × 卫星"10 分钟内不重复弹窗,防的是"告警风暴瞬间糊满屏幕"。 - **未读角标**:铃铛上的红点计数来自 store,超过 99 显示 99+,归零自动隐藏。  面板本身很简单:未读 / 全部两个视图、"全部已读"、点条目自动标注已读并跳转告警中心。真正花心思的是上面这些"行为边界"——什么时候弹、弹多久、丢消息怎么办、风暴时怎么压。 ## 第 4 站 · 闭环:"已确认"不等于"已解决" 这是链路第 5 秒发生的事:人打开详情、按下"确认处置"。我们把状态机设计成三个状态,各自有清晰的语义: - **FIRING(触发中)**:系统在喊,没有人回应; - **ACKNOWLEDGED(已确认)**:有人接单了——**不代表问题已解决**,只代表"人看到了,正在处理"; - **RESOLVED(已恢复)**:指标自己回到了正常范围(由下一轮评估自动判定,不需要人工点)。 确认处置的三条防线,全部围绕"状态机的尊严": 1. **已恢复的不许再确认**(返回 400)——问题都没了,别补盖章; 2. **重复确认是幂等的**——不覆盖首次确认人和确认时间,接单只接一次; 3. **确认时顺带清未读**——处置过的事不该继续顶着未读角标。 还有个容易被忽略的一致性细节:**确认之后,如果指标还在持续越限,评估引擎只刷新数值,不把状态打回 FIRING**——人工处理中的告警,机器不能抢方向盘。等指标真正恢复正常,统一转"已恢复",而"当初谁确认的"依然留在记录里。  这套闭环不是靠手点验证的。我们专门写了一条"**真实后端**"端到端用例:浏览器真实登录 → 通知中心建立真实 STOMP 连接(断言"实时推送已连接")→ 经 REST 灌入越限数据 → 评估引擎真实广播 → 断言真实弹窗内容(规则名 + `snr=-5.00 below threshold 0.00`)→ 未读角标 +1 → 详情页自动已读 → 确认处置闭环。全部断言跑在真实后端、真实数据库、真实 WebSocket 上——第 2 站那个 Bug,正是这类用例存在的理由。 验收当天还做了一次"全科考试":五类规则(阈值 / 区间 / 持续时长 / 联合 / 趋势)同轮全部触发(趋势规则的斜率 **-32.74/分钟** 被逮个正着),随后逐条确认、灌入恢复样本,五条记录在同一轮评估里全部自动转"已恢复"。  ## 第 5 站 · SU4:遥测健康评估升级——联合判读与趋势预警 前四站把"一次告警的完整生命周期"讲完了。这一站回答两个新问题:**只有单指标比较够用吗?没有。** **问题一:有些异常,单指标永远看不出来。** "舱温偏高"和"电池放电电流偏高"单独出现都可能只是正常波动;两个同时出现,才是真正值得叫醒人的信号。于是有了 COMPOSITE 规则:条件数组(JSONB 存储、2~5 条、每条支持 GT/LT/OUTSIDE/INSIDE)按 AND / OR 汇总。语义上抠了两个细节: - **AND**:全部条件命中才算越限;任一指标缺失 → SKIP(无法判定)。不能因为"半个条件没数据"就触发,同样不能因为"半个条件没数据"就误报恢复。 - **OR**:任一条件命中即越限;缺失指标的条件不参与判定;所有条件都拿不到值时才算 SKIP。 触发消息会列出参与判定的条件快照(AND 列全部、OR 列命中的),超过 500 字符自动截断——500 刚好是消息列宽,库和消息两端不会打架。 **问题二:等越线再喊,有时候已经晚了。** 电池从 90% 掉到 80% 用了一小时,和用十分钟掉下去,性质完全不同。TREND 规则盯的就是**变化率**:取回看窗口(30 秒~24 小时,可配)内的历史样本,做最小二乘线性回归,求出斜率(换算成"指标单位/分钟",带符号),再跟斜率阈值比较。`LT -0.5` 的含义是"下降速度快于 0.5/分钟"——数值还没跌破任何静态阈值,预警就已经发出。 两次"不硬算"的克制:有效样本少于 3 条直接跳过(样本太少,斜率全是噪声);窗口内样本上限 120 条、取最近的一批(遥测高频上报时,单轮评估不能被一条趋势规则拖慢)。  架构上做的最重要的一件事是留**扩展点**。判定逻辑被抽成策略族:`RuleBreachEvaluator` 接口 + 按 `rule_type` 分派的注册表,原有两个类型(阈值、区间)从评估引擎里抽出来成为独立策略类(行为不变、先补测试再搬运)。**以后新增一类判读规则 = 新增一个策略类 + 注册表登记一行,评估主流程一行不改。** 这次从 2 类扩到 4 类,验证的就是这个扩展点:评估引擎本身没有改动,新增逻辑全部落在新文件里。 ## 第 6 站 · 数据管理中心:从外部编目"进货" 链路讲完了,最后看一条完全不同的数据流。平台的卫星档案原来靠自己录;这次追加了一个"数据管理中心"(仅管理员可见):**从外部编目数据源(CelesTrak、Space-Track、KeepTrack)把星座与卫星档案批量"进货"进来。**  三种数据源的"脾气"完全不同,这恰恰是架构上最费心思的地方: - **CelesTrak**:公开编目,免凭证;按分组(空间站、星链、GPS……)查询,解析的是"卫星名 + 两行 TLE"组成的三行组;还带着第三方站点的 2 小时节流——正常使用时如果踩到 403,会被映射成"数据源忙碌,请稍后再试"的人话提示,而不是把上游故障原样砸给用户; - **Space-Track**:要账号密码;走 `/ajaxauth/login` 会话登录拿 Cookie,并且做了**请求级 Cookie 隔离**(会话不污染全局连接池); - **KeepTrack**:返回的 JSON 有"薛定谔的形态"——`{data: []}`、单个对象、裸数组都可能出现,字段名还有别名。解析层先全部容错,再统一收敛。 无论上游多参差,出口只有一个模型:`CatalogEntry` 归一化条目(NORAD 编号、名称、国际编号、倾角、偏心率、轨道分类……)。各数据源实现 `CatalogProvider` 接口(声明键、展示名、参数表单元数据、拉取逻辑),注册表自动发现——**新增第四个数据源 = 写一个实现类,前端的参数表单会自动按元数据渲染,预览、导入、历史全部零改动复用。** 前端流程(见图):选数据源 → 填参数(CelesTrak 免凭证直接拉)→ 拉取预览 → 勾选 → 导入设置 → 执行 → 历史留痕。预览里每个条目标注"新条目 / 已存在"——跟本地库按 NORAD 编号**批量**比对得出,不是逐条查库。  上面这张图是真实拉取的现场:ISS(ZARYA)、天和核心舱、问天实验舱……20 条真实编目条目一目了然,轨道类型已经自动分好类(按高度/倾角判 LEO/MEO/GEO)。 导入环节三个防止"帮倒忙"的开关: - **已存在的卫星**:`跳过(仅新增)` 或 `更新轨道要素`——更新模式只刷轨道类信息,你在平台上人工维护的字段不会被外部数据整行覆盖; - **归入星座**:可选把新卫星归入指定星座;星座不存在时**自动建档**(用成员卫星的均值轨道要素建一个,并刷新成员计数); - **留痕**:每次导入都写一条 append-only 记录(数据源、参数、模式、新增/更新/跳过/失败计数、操作人、耗时),接口层再叠一层操作审计。 最后是安全底线:入口做了三层管控(菜单隐藏、路由重定向、接口上类级 `@PreAuthorize` 锁 ADMIN);外部数据源的账号密码**不进仓库**——本地开发放 `leo-local.yml`(已被 gitignore 排除),部署走环境变量,仓库里只有示例模板。 ## 收尾:本次交付的数字 - 后端单测:**172/172**(系列上篇时还是 46 条:判读策略新增 22 条、数据管理中心新增 34 条,其余为评估引擎与规则校验扩展); - 前端门禁:eslint + vue-tsc **0 error**; - 端到端:**13 passed + 4 skipped**;其中"真实后端"告警链路用例 1 passed(真登录、真 WebSocket、真广播、真落库); - 闭环演示:五类规则同轮触发,一次通过(含 -32.74/分钟 的斜率预警); - 真实链路揪出的缺陷:1 个(`is_read` 落库,已修复并补回归断言)。 这套链路里没有一件东西是"卫星专属":规则的库层校验、评估的 SKIP 三态与去重、通知的"推送 + 拉取兜底"双轨、确认处置的状态机防线——任何"让系统主动找人"的场景都能整段搬走。 文中的截图全部来自真实系统运行(演示规则已在出图后删除,记录保留)。如果这篇帮你绕开哪怕一个坑,欢迎转发给正在设计告警的同事。留言区想聊聊:你们系统里"已确认未恢复"这种中间态,是保留独立的确认人信息,还是用完即弃? ***本文关键词:告警规则引擎 | 评估引擎 | STOMP/WebSocket 推送 | 通知中心 | 状态机闭环 | 最小二乘趋势预警 | JSONB 联合判读 | 数据编目导入 | SPI 扩展点 | 卫星平台 2.0***
Make it right, Make it work, Make it better, Make it best
**继续提炼、完善,据说到了企业级代码的门前** ```/** * 1、常量 * 1.1 菜单 * 1.1.1 菜单编号 * 1.1.2 菜单描述 * 1.1.3 装修套餐单价 * <p> * 1.2 正则 * 1.2.1 面积:拦截负数,非数字 * 1.2.2 菜单:只放行 0-3 * <p> * 1.3 提示词 * 1.3.1 面积输入 * 1.3.2 菜单输入 * 1.3.3 退出 * 1.3.4 空输入(面积和菜单) * 1.3.5 面积无效: * 1.3.6 面积 0 * 1.3.7 菜单无效 * 1.3.8 返回上一层 * <p> * 1.4 哨兵码表 * 1.4.1 -1:退出 * 1.4.2 -2:空输入 * 1.4.3 -3:无效输入 * 1.4.4 0:面积 0,输入合法,业务不合法,业务层拦截 * 1.4.5 0:菜单 0,返回面积层 * <p> * 1.5 格式 * 1.5.1 报价单编号 -0001 * 1.5.2 报价单前缀 BJ2026 * 1.5.3 报价单日期 * 1.5.4 材料费占比 * <p> * 1.6 报价单文本块 * <p> * 1.7 报价单字段 * <p> * 2、main()步骤 * 2.1 面积输入 * <p> * 2.2 菜单输入 * <p> * 3 报价单 * 3.1 报价单快照(获取报价单字段) * 3.1.1 获取报价单编号 * 3.1.2 获取报价单日期 * 3.1.3 获取房屋面积 * 3.1.4 获取装修套餐的编号 * 3.1.5 获取装修套餐编号对应的描述 * 3.1.6 获取装修套餐编号对应的单价 * 3.1.7 计算装修套餐的金额 * 3.1.8 计算材料费用 * 3.1.9 计算人工费用 * <p> * 3.2 报价单输出 * <p> * 4、方法 * 4.1 getHouseArea() * 4.2 judgeHouseArea() * 4.3 getMenuItemNumber() * 4.4 judgeMenuItemNumber() * 4.5 getMenuItemDesc() * 4.6 getMenuItemUnitPrice() * 4.7 getQuotationNumber() * 4.8 calcTotalAmount() * 4.9 getQuotationCreateDateTime() * 4.10 snapshotQuotation() * 4.11 printQuotation() * */ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Scanner; public class Day27RewriteQuoteTextBlock { private static final String APP_EXIT_WORD = "EXIT"; // 退出程序的关键词 // 装修套餐菜单编号、描述、单价 private static final int MENU_NUMBER_ECONOMY = 1; private static final int MENU_NUMBER_COMFORT = 2; private static final int MENU_NUMBER_LUXURY = 3; private static final String MENU_ITEM_DESC_ECONOMY = "经济套餐"; private static final String MENU_ITEM_DESC_COMFORT = "舒适套餐"; private static final String MENU_ITEM_DESC_LUXURY = "豪华套餐"; private static final double MENU_ITEM_UNIT_PRICE_ECONOMY = 800; private static final double MENU_ITEM_UNIT_PRICE_COMFORT = 1200; private static final double MENU_ITEM_UNIT_PRICE_LUXURY = 1800; // 面积和菜单编号输入的正则表达式 private static final String AREA_REGEX = "^[0-9]+(\\.[0-9]+)?$"; private static final String MENU_NUMBER_REGEX = "^[0-3]$"; // 程序的提示词 private static final String APP_TITLE = "装修报价管理系统(QMS)"; private static final String INPUT_AREA_TIP = "请输入面积(m²)【exit-退出】:"; private static final String INPUT_MENU_TIP = "请选择装修套餐(1-经济套餐|2-舒适套餐|3-豪华套餐)【0-返回上一层】:"; // 面积输入、菜单输入的提示词 private static final String INPUT_IS_BLANK_TIP = "没有输入,请重新输入!"; private static final String INPUT_EXIT_TIP = "Goodbye,See you next time."; private static final String INPUT_INVALID_TIP = "无效的输入,请重新输入!"; private static final String INPUT_AREA_ZERO_TIP = "面积必须大于 0 ,请重新输入!"; private static final String INPUT_MENU_ZERO_TIP = "返回上一层,请重新输入面积!"; private static final int SENTINEL_EXIT = -1; // 退出 private static final int SENTINEL_IS_BLANK = -2; // 空输入 private static final int SENTINEL_INVALID = -3; // 无效的输入 private static final int SENTINEL_AREA_ZERO = 0; // 面积0 private static final int SENTINEL_MENU_ZERO = 0; // 菜单0 // 报价单前缀、后缀格式、日期格式 private static final String PREFIX_QUOTATION_NUMBER = "BJ2026"; private static final String SUFFIX_QUOTATION_NUMBER_FORMATTER = "-%04d"; private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // 报价单字段 private static double houseArea = 0; private static final double RENOVATION_MATERIAL_COST_RATIO = 0.6; private static int renovationPackageLevel = 0; private static String renovationPackageDesc = ""; private static double renovationPackageUnitPrice = 0; private static double renovationTotalAmount = 0; private static String quotationNumber = ""; private static String quotationCreateDateTime = ""; private static double quotationHouseArea = 0; private static double renovationMaterialCost = 0; private static double renovationLaborCost = 0; private static double renovationOtherCost = 0; private static final String PRINT_BILL_TEXT = """ ==============================================装修套餐报价单============================================== 报价编号:%s 报价日期:%s ------------------------------------------------------------------------------------------------------- 房屋面积:%,.2f m² 装修套餐:%s ------------------------------------------------------------------------------------------------------- 套餐单价:%,.2f 元/m² 装修总价:%,.2f 元 其中: 材料费用:%,.2f 元 人工费用:%,.2f 元 其它费用:%,.2f 元 ------------------------------------------------------------------------------------------------------- """; private static int quotationCount = 0; // 报价单编号计数器 public static void main(String[] args) { // 面积输入 Scanner in = new Scanner(System.in); while (true) { System.out.println(APP_TITLE); System.out.print(INPUT_AREA_TIP); String area = in.nextLine().strip(); houseArea = getHouseArea(area); // 是否退出 if (houseArea == SENTINEL_EXIT) { System.out.println(INPUT_EXIT_TIP); break; } if (!judgeHouseArea(houseArea)) { continue; } // System.out.println("获取到的房屋面积是:" + houseArea + " m²"); // 菜单输入 int menuNumber; while (true) { System.out.print(INPUT_MENU_TIP); String selected = in.nextLine(); menuNumber = getMenuItemNumber(selected); // 是否返回上一层 if (menuNumber == SENTINEL_MENU_ZERO) { System.out.println(INPUT_MENU_ZERO_TIP); break; } if (judgeMenuItemNumber(menuNumber)) { renovationPackageLevel = menuNumber; // 报价单 // 报价单快照 snapshotQuotation(houseArea, renovationPackageLevel); // 报价单输出 printQuotation(); break; } } } } // 方法 /** * 按格式输出报价单 */ private static void printQuotation() { String quoteText = String.format(PRINT_BILL_TEXT, quotationNumber, quotationCreateDateTime, quotationHouseArea, renovationPackageDesc, renovationPackageUnitPrice, renovationTotalAmount, renovationMaterialCost, renovationLaborCost, renovationOtherCost); System.out.println(quoteText); } /** * 报价单快照 * * @param houseArea * @param renovationPackageLevel */ private static void snapshotQuotation(double houseArea, int renovationPackageLevel) { quotationNumber = getQuotationNumber(); quotationCreateDateTime = getQuotationCreateDateTime(); quotationHouseArea = houseArea; renovationPackageDesc = getMenuItemDesc(renovationPackageLevel); renovationPackageUnitPrice = getMenuItemUnitPrice(renovationPackageLevel); renovationTotalAmount = calcTotalAmount(quotationHouseArea, renovationPackageUnitPrice); renovationMaterialCost = renovationTotalAmount * RENOVATION_MATERIAL_COST_RATIO; renovationLaborCost = renovationTotalAmount - renovationMaterialCost; renovationOtherCost = 0; } /** * 计算装修套餐的总金额(总金额 = 房屋面积 * 装修套餐对应的单价) * * @param houseArea * @param renovationPackageUnitPrice * @return */ public static double calcTotalAmount(double houseArea, double renovationPackageUnitPrice) { return houseArea * renovationPackageUnitPrice; } /** * 获取报价单创建日期时间 * * @return */ public static String getQuotationCreateDateTime() { return LocalDateTime.now().format(DATE_TIME_FORMATTER); } /** * 获取报价单编号 * * @return */ public static String getQuotationNumber() { quotationCount++; String suffixQuotation = String.format(SUFFIX_QUOTATION_NUMBER_FORMATTER, quotationCount); return (PREFIX_QUOTATION_NUMBER + suffixQuotation); } /** * 获取装修套餐菜单编号对应的单价 * * @param menuNumber * @return */ public static double getMenuItemUnitPrice(int menuNumber) { return switch (menuNumber) { case MENU_NUMBER_ECONOMY -> MENU_ITEM_UNIT_PRICE_ECONOMY; case MENU_NUMBER_COMFORT -> MENU_ITEM_UNIT_PRICE_COMFORT; case MENU_NUMBER_LUXURY -> MENU_ITEM_UNIT_PRICE_LUXURY; default -> 0; }; } /** * 获取装修套餐菜单编号对应的描述 * * @param menuNumber * @return */ public static String getMenuItemDesc(int menuNumber) { return switch (menuNumber) { case MENU_NUMBER_ECONOMY -> MENU_ITEM_DESC_ECONOMY; case MENU_NUMBER_COMFORT -> MENU_ITEM_DESC_COMFORT; case MENU_NUMBER_LUXURY -> MENU_ITEM_DESC_LUXURY; default -> "返回上一层"; }; } /** * 判断装修套餐菜单编码的输入是否合法,true=合法放行,false=提示应重输 * * @param menuNumber * @return */ public static boolean judgeMenuItemNumber(int menuNumber) { // 是否重新输入,空输入 if (menuNumber == SENTINEL_IS_BLANK) { System.out.println(INPUT_IS_BLANK_TIP); return false; } // 是否重新输入,无效的输入 if (menuNumber == SENTINEL_INVALID) { System.out.println(INPUT_INVALID_TIP); return false; } return true; } /** * 获取装修套餐的菜单编号,通过菜单编号获取对应套餐的描述和单价 * * @param menuSelected * @return */ public static int getMenuItemNumber(String menuSelected) { int menuNumber; // 空输入判断 if (menuSelected.strip().isBlank()) { return SENTINEL_IS_BLANK; } if (menuSelected.strip().matches(MENU_NUMBER_REGEX)) { menuNumber = Integer.parseInt(menuSelected); } else { return SENTINEL_INVALID; } return menuNumber; } /** * 判断房屋面积的内容是否合法,true=合法放行,false=提示应重输 * * @param houseArea * @return */ public static boolean judgeHouseArea(double houseArea) { // 是否重新输入,空输入 if (houseArea == SENTINEL_IS_BLANK) { System.out.println(INPUT_IS_BLANK_TIP); return false; } // 是否重新输入,无效的输入 if (houseArea == SENTINEL_INVALID) { System.out.println(INPUT_INVALID_TIP); return false; } // 是否0输入,特殊提示 if (houseArea == SENTINEL_AREA_ZERO) { System.out.println(INPUT_AREA_ZERO_TIP); return false; } return true; } /** * 获取房屋面积,返回哨兵码值或房屋面积 * * @param area * @return */ public static double getHouseArea(String area) { // 空输入判断 if (area.strip().isBlank()) { return SENTINEL_IS_BLANK; } // "exit"判断 if (APP_EXIT_WORD.equalsIgnoreCase(area)) { // APP_EXIT_WORD = "EXIT" return SENTINEL_EXIT; } // 正则判断 if (!area.strip().matches(AREA_REGEX)) { return SENTINEL_INVALID; } houseArea = Double.parseDouble(area); return houseArea; } } ```
AI Coding 已经改变开发方式了,程序员到底还该学什么?
# 关于 AI 应用和现在程序员就业的一些看法 前段时间有朋友问我: **“现在 AI 这么火,要不要从 Java 转 AI 应用?”** 我当时第一反应是,这两个东西其实就不应该放在一起比较。 有点像几年前问: **“我要不要从 Java 转电商?”** AI 应用是一个方向,Java 是后端的一门语言,本身就不是一个维度的东西。 借这个问题,说一下我目前对 AI 应用、技术栈和程序员就业的一些看法。 都是这两年工作下来的一些个人感受,略带主观。 --- ### 1. 单一技术栈已经不太够用了 国内 Java 的存量盘依旧很大,这个短时间内不会有什么变化。 但我觉得现在只会 Java,或者把自己完全定义成一个 Java 工程师,已经不太够了。 目前后端我比较建议接触的还是: **Java、Go、Python。** Java 主要还是国内大量存量项目、企业级应用和现有团队技术栈。以后很多传统业务做 AI 赋能,也不可能把原来的东西全部推倒重来。 Go 在高并发、云原生这些场景本身就有优势。现在很多 AI 应用又恰好涉及大量并发、流式输出和长连接,用 Go 也比较合适。 Python 就不用说了。 AI 应用继续往下走,不管是 PyTorch、训练、推理还是 AI Infra,基本都绕不开。 当然我不是说三门语言都要学到一样深。 **至少有一门是自己的主语言,真正熟练,其他的能用、能看、能改就行。** 现在有 AI 以后,第二门、第三门语言的学习成本其实已经低很多了。 前端也是一样。 以前如果有人问我国内学 Vue 还是 React,我可能会更建议 Vue。 现在我会更建议 **React + TypeScript**。 主要还是国外生态更大,而且现在 AI Coding 对 React/TS 的支持也更好。 当然结合国内就业,Vue 最好还是得会。 所以我现在对技术栈的看法比较简单: **主技术栈一定要有,但没必要再给自己贴死某一门语言的标签。** --- ### 2. 一定要高强度使用 AI 这个是我目前最确定的一点。 AI Coding 已经不是“以后会不会普及”的问题了。 已经发生了。 我们目前甚至有一个真实的企业项目,基本就是纯 Vibe Coding 驱动开发。 而且不是 Demo。 项目本身是一个比较复杂的 AI 应用平台,涉及多租户、异步任务、分布式并发控制、实时通信、向量检索、全文检索、消息网关、服务监控和 CI/CD 等。 AI 这块也不只是调一下大模型 API,还涉及 RAG、多模态、ASR、视觉检索,以及 PyTorch、CUDA 相关的推理服务。 并且整个架构从一开始就是按照服务拆分、异步化和后续横向扩展去设计的。 说这些不是想证明这个项目有多复杂。 主要是想说,**这是一个有真实工程复杂度、需要长期维护的企业项目,而且大量代码确实是 AI 完成的。** 到目前为止,也没有出现所谓“Vibe Coding 到最后完全没法维护”的情况。 所以我现在觉得: **Vibe Coding 本身不是问题,问题是使用 AI 的人有没有工程能力。** 前两天我负责对接一个渠道商做兼容性的 RAD 开发。 方案、编码、测试基本全部由 AI 完成。 大概半天。 这个事情如果按照以前的开发方式,我估计至少两三周。 所以对于 AI,我没什么“要不要用”的建议。 **一定要用,而且要高强度地用。** 方案、编码、测试、Debug、Review、CI/CD,能用就用。 这已经是生产力问题了。 再说句题外话。 我现在觉得 AI 已经不只是开发工具了。 生活里很多需要搜索信息、学习、分析、做决策的事情,我都会先问一下 AI。 不一定听它的,但至少多一个信息源,多一个思考角度。 开发就更不用说了。 现在越来越多公司已经把 AI Coding 当成正常的研发工具,AI 带来的提效也是实打实的。 所以如果你长期处在一些接触不到 AI 的开发环境里,比如部分外包、纯内网、对日或者 ToG 项目,我觉得需要注意一个问题: **你损失的不只是 AI 帮你写代码的那点效率,而是在慢慢错过一套新的研发方式。** 短期当然没什么。 但如果三年、五年一直处在这种环境里,而外面的工程师已经习惯用 AI 做方案、编码、测试、Review、Debug,甚至完成整个研发流程,那两边积累下来的工作方式和效率差距会越来越大。 所以如果工作环境确实用不了 AI,我反而更建议自己在工作之外保持使用。 **可以不用 AI 写公司的代码,但不要让自己长期脱离 AI。** 这也是为什么我一直强调高强度使用 AI。 不是因为现在 AI 火。 而是我觉得,**会不会把 AI 真正融入自己的工作流,很可能会逐渐变成工程师的基础能力。** --- ### 3. AI 越强,基本功反而越重要 这一点我最近感受也比较深。 24 年甚至更早的时候,我们是真的会为了一个环境折腾半天。 碰到一些难调的 Bug,就自己看日志、翻源码、抓包、排调用链,一点一点 Debug。 现在很多问题直接扔给 AI,可能几分钟就解决了。 所以有时候我反而觉得,26 年才开始学编程的人可能更难。 不是因为工具差,恰恰是因为**工具太好了。** 很多以前必须自己经历的过程,现在可以直接跳过去。但那些过程其实会慢慢形成一些工程直觉。 所以如果你是新人,问我要不要打好基础,我的答案是: **非常有必要。** 数据结构、操作系统这些基础该学还是得学。 同时至少选一门自己的主开发语言,真正学到熟练。不是会写几个接口、会调几个框架就叫熟练,而是真的拿它做过项目、调过问题,对它的运行机制和生态有足够的理解。 因为 AI 可以写代码,也可以 Debug。 **但 AI 给你的东西到底对不对,最后还是需要你判断。** 事务、并发、幂等、异常、性能、可观测性,包括系统出了问题应该从哪里开始查,这些东西不会因为 AI 出现就消失。 反而是 AI 把编码本身变得越来越便宜以后,**这些东西才是真正能拉开工程师深度的地方。** 以前没有 AI 的时候,大家卷框架、卷 API、卷谁记得多,我觉得没什么问题,因为当时这些东西确实直接影响开发效率。 但现在很多框架层面的东西,AI 比我们记得更全,写得也更快。 所以我现在反而更建议新人把时间往基础上放一点。 当然,这个建议也不只是给新人。 **包括我自己,现在也在重新花更多精力,更专业、更体系化地去学这些基础。** 以前很多东西可能是工作里遇到了再学,或者知道怎么用就够了。但现在有了 AI,我反而觉得可以把以前花在记框架、记 API 上的时间拿回来,真正去理解一些更底层、更长期的东西。 有几本书我一直比较推荐: 1. Clean Code 2. The Clean Coder 3. Understanding the Linux Kernel 4. Head First Design Patterns 5. Designing Data-Intensive Applications 当然,书只是一个载体。 我真正想表达的是: **AI 时代不是不需要基本功了,而是以前很多“会用框架”的价值正在被 AI 吃掉,基础和工程判断的价值反而更高了。** AI 可以替你少走很多弯路,但有些路你最好真的走过。 所以我现在越来越觉得,AI 真正放大的不是编码能力。 **是判断能力。** --- ### 4. AI 应用可以转,但现在已经不是做几个 Demo 就好找工作的阶段了 我自己其实算比较早转 AI 应用的。 **25 年 10 月,我就从传统开发转到 AI 赛道了。** 所以如果提前半年或者一年有人问我: “会 RAG、做几个 Agent 项目,能不能转 AI 应用?” 我会觉得问题不大。 但现在我的看法已经变了。 不是 AI 应用不值得做,恰恰是因为**这个方向太火了,进来的人太多了。** 现在很多简历翻来覆去都是: RAG 知识库、AI 客服、PDF 问答、LangChain、Embedding、Milvus、Rerank…… 这些东西当然要会,我自己也在做。 但问题是,**会这些东西的人已经太多了。** 更现实一点说,现在如果只是拿几个这种项目去找 AI 应用的工作,我觉得已经没有以前那么容易了。 而且关于项目这件事,我可以说得直接一点: **现在国内网上能看到的大量所谓 AI 应用项目,我觉得本质上还是玩具项目。** 换一个模型、接一个知识库、套一个 Agent 框架,最后做个聊天页面。 能跑。 但离真正的企业项目还很远。 所以如果现在还准备转 AI 应用,我更建议去看: **真实业务、真实客户、真实场景。** 我前公司是做 AI + 教育的。 国内这一块真正落地的东西,无非也是 AI 答疑、AI 批改、OCR/VLM、个性化学习这些。 AI 答疑底层一样可能是 RAG。 区别只是它最后真的要面对学生、老师和学校,真的要解决问题。 所以不是不要学 RAG,也不是不要学 Agent。 **这些东西该学还是得学,只是别再把“会 RAG、会调 Agent”本身当成竞争力了。** --- ### 5. 除了业务深度,还需要一点技术纵深 上面说的是业务。 但只解决“项目别做成 Demo”这一个问题,我觉得还不够。 现在 AI Application / Agent 这个方向已经很热了,而且这个趋势越来越明显。 当大量工程师都开始做 Agent、RAG、工作流、MCP 以后,应用层的技术差异一定会越来越小。 所以除了去做真实业务以外,我现在还有另外一个建议: **适当往下走,给自己增加一点技术纵深。** 这也是我自己接下来准备深入 AI Infra 的原因。 我目前比较看好: **后端 / 分布式 + AI Application / Agent + AI Infra。** 不是放弃应用层。 恰恰相反,我觉得上层的业务经验很重要,只是在这个基础上继续往模型推理和基础设施走一点。 当然,AI Infra 也别最后学成下一个 RAG。 不是会 Docker、K8s、vLLM,再部署一下 CUDA 环境,就叫 AI Infra。 继续往下还有 Inference Serving、Batching、Scheduling、KV Cache、GPU Memory、PyTorch、CUDA、NCCL、GPU Cluster,以及监控、扩缩容、容错、成本这些东西。 这一块我自己也还在继续学,所以不展开,也不装懂。 我现在的想法其实很简单: **应用层越来越拥挤,就不要只在应用层继续横向堆东西。** 业务上往真实场景走,技术上往深处走。 至少目前,我自己准备这么走。 --- 最后其实就两个建议。 **1. 高强度使用 AI,别和生产力过不去。** 不要只拿 AI 补两行代码。 方案、编码、测试、Debug、Review、CI/CD,能用就用。 **2. 如果准备转 AI,别再堆玩具 Demo。** 去看真实业务,解决真实问题,再给自己找一个方向往深了走。 至于 Java、Go、Python、React、Vue,该用什么就用什么。 **主技术栈要有,工程基本功别丢,AI 狠狠用。** 差不多就这些。 听不听随意。
清爽的main()结构
**封装了一个面积输入判断的方法,调整了几个细节** ```/** * 菜单循环+方法化+输入校验+三档单价+输出明细(材料/人工/总价)。完成后关掉参考代码,从零再写一遍 * <p> * 注意: * 1、Quote 类(9 字段)+ static 编号自增 + 日期 * 2、printQuote(quote) 输出编号/日期/面积/档次/单价/材料/人工/总价 * 3、验收三关 + 连续出 3 单看编号递增 * 4、【从零重写】关参考、凭笔记默写、计时只记录 * <p> * class Quote { * int seq; // 对应未来主键 id(现为内存序号) * String quoteNo; // 报价单编号 BJ2026-0001(业务编号) * String createDate; // 对应 create_time * double area; // 对应 area * int level; // 对应 level * double unitPrice; * double materialCost; * double laborCost; * double totalPrice; // 对应 total_price * // userId 字段先空着——那是 W26 用户系统的事,TODO 标注 * } * static void printQuote(Quote q) { * String bill = """ * ============ 装修报价单 ============ * 报价编号:%s * 报价日期:%s * ------------------------------------ * 房屋面积:%.2f m² * 装修套餐:%s * 套餐单价:%,.2f 元/m² * ------------------------------------ * 材料费用:%,.2f 元 * 人工费用:%,.2f 元 * ==================================== * 总价:%,.2f 元 * ==================================== * """.formatted(q.quoteNo, q.createDate, q.area, * q.desc, q.unitPrice, q.materialCost, * q.laborCost, q.totalPrice); * System.out.print(bill); * } * */ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Scanner; public class Day27QuoteTextBlock { private static final int MENU_NUMBER_ZERO = 0; private static final int MENU_NUMBER_ECONOMY = 1; // 菜单编号:1 private static final int MENU_NUMBER_COMFORT = 2; // 菜单编号:2 private static final int MENU_NUMBER_LUXURY = 3; // 菜单编号:3 private static final String LEVEL_ECONOMY_DESC = "经济套餐"; // 套餐描述 private static final String LEVEL_COMFORT_DESC = "舒适套餐"; // 套餐描述 private static final String LEVEL_LUXURY_DESC = "豪华套餐"; // 套餐描述 private static final double LEVEL_ECONOMY_UNIT_PRICE = 800; // 经济套餐单价:800 private static final double LEVEL_COMFORT_UNIT_PRICE = 1200; // 舒适套餐单价:1200 private static final double LEVEL_LUXURY_UNIT_PRICE = 1800; // 豪华套餐单价:1800 private static final String DOUBLE_REGEX = "^[0-9]+(\\.[0-9]+)?$"; // 面积输入的正则表达式 private static final String MENU_INT_REGEX = "^[0123]$"; // 装修档次菜单编号输入的正则表达式 private static final String PREFIX_ORDER_NO = "BJ2026"; // 订单编号的前缀 private static final String ORDER_NUMBER_FORMAT = "-%04d"; // 订单编号的格式 private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final double MATERIAL_RATIO = 0.6; // 设置材料费用在总价中的占比 private static int orderNumberCount = 0; // 订单号码自动编号计数器 private static String quoteNo = ""; // 报价单编号 BJ2026-0001(业务编号) private static String createDate = ""; // 对应 create_time 报价单创建日期 private static double area = 0; // 对应 area 房屋面积 private static int level = 0; // 对应 level 装修档次 private static double unitPrice = 0; // 对应 unitPrice 装修单价 private static double materialCost = 0; // 对应 level 材料费用 private static double laborCost = 0; // 对应 level 人工费用 private static double otherCost = 0; // 对应 level 其他费用 private static double totalPrice = 0; // 对应 total_price 总金额 private static final int RETURN_VALUE_INPUT_ZERO = 0; // 返回值为0,就是输入为0 private static final int RETURN_VALUE_INPUT_EXIT = -1; // 返回值为-1,就是要退出程序 private static final int RETURN_VALUE_INPUT_IS_BLANK = -2; // 返回值为-2,就是没有输入 private static final int RETURN_VALUE_INPUT_INVALID = -3; // 返回值为-3,就是输入无效 private static final int RETURN_VALUE_INPUT_INVALID_MENU_NUMBER = -3; // 返回值为-3,就是菜单编号输入无效 private static final String PRINT_TITLE = "----------------------------------装修报价单----------------------------------"; private static final String INPUT_AREA_TIP = "请输入房屋面积(m²),回车确定|exit退出:"; private static final String INPUT_MENU_TIP = "请选择装修套餐(1-经济|2-舒适|3-豪华):"; private static final String AREA_EXIT_TIP = "感谢使用,See you next time."; // 程序退出的提示 private static final String AREA_IS_BLANK_TIP = "没有输入,请重新输入面积(m²)!"; // 没有输入面积的提示 private static final String AREA_INVALID_TIP = "输入了无效的面积,请重新输入面积(m²)!"; // 面积输入无效的提示 private static final String MENU_NUMBER_INVALID_TIP = "菜单编号输入无效,请输入装修套餐(1-经济|2-舒适|3-豪华)!"; // 装修档次菜单编号输入无效的提示 private static final String AREA_ZERO_TIP = "面积要大于0,请重新输入面积(m²)!"; // 面积为0的提示 private static final String PRINT_BILL = """ ================================================= 装修报价单 ================================================= 报价编号:%s 报价日期:%s ------------------------------------------------------------------------------------------------------------ 房屋面积:%.2f m² 装修套餐:%s 套餐单价:%,.2f 元/m² ------------------------------------------------------------------------------------------------------------ 材料费用:%,.2f 元 人工费用:%,.2f 元 其他费用:%,.2f 元 ============================================================================================================ 总 价:%,.2f 元 ============================================================================================================ """; public static void main(String[] args) { Scanner in = new Scanner(System.in); while (true) { System.out.print(PRINT_TITLE + "\n"); System.out.print(INPUT_AREA_TIP); // 请输入面积,exit-退出 String line = in.nextLine().strip(); // 判断面积,合法的话(返回值为>0)就进行菜单展示和选择 double houseArea = getHouseArea(line); if (houseArea == RETURN_VALUE_INPUT_EXIT) { // 判断返回值,-1就退出 System.out.println(AREA_EXIT_TIP); break; } if (!judgeHouseArea(houseArea)) { // 判断返回值,如果非法,就一直循环 continue; } // 菜单流程 int menuNum; while (true) { System.out.print("\n" + INPUT_MENU_TIP); String num = in.nextLine(); level = getMenuNumber(num); if (level >= MENU_NUMBER_ECONOMY && level <= MENU_NUMBER_LUXURY) { break; } System.out.print(MENU_NUMBER_INVALID_TIP); } // 报价单输出 snapshotQuote(houseArea, level); printQuote(); } } /** * 判断面积输入是否合法,返回true/false,交由main主方法做下一步处理 * <p> * RETURN_VALUE_INPUT_EXIT这个值在getHouseArea中返回,在main中先执行,所以这个值在本方法中无法触达 * * @param houseArea * @return */ public static boolean judgeHouseArea(double houseArea) { if (houseArea == RETURN_VALUE_INPUT_INVALID) { System.out.println(AREA_INVALID_TIP); return false; } if (houseArea == RETURN_VALUE_INPUT_IS_BLANK) { System.out.println(AREA_IS_BLANK_TIP); return false; } if (houseArea == RETURN_VALUE_INPUT_ZERO) { System.out.println(AREA_ZERO_TIP); return false; } // 包含了case RETURN_VALUE_INPUT_EXIT也直接返回,交由main方法来 return true; } /** * 报价单字段的快照 * * @param houseArea * @param level */ public static void snapshotQuote(double houseArea, int level) { // 字段计算 quoteNo = getOrderNumber(); // 订单编号 createDate = LocalDateTime.now().format(DATE_TIME_FORMAT); area = houseArea; // 获取面积 unitPrice = getUnitPrice(level); // 获取单价 totalPrice = calcTotalAmount(houseArea, unitPrice); // 总金额 materialCost = totalPrice * MATERIAL_RATIO; // 材料费用 laborCost = totalPrice - materialCost; otherCost = 0; // 其他费用,暂时定为0 } /** * 输出报价单 */ public static void printQuote() { String quoteText = String.format(PRINT_BILL, quoteNo, createDate, area, getLevelDesc(level), unitPrice, materialCost, laborCost, otherCost, totalPrice); System.out.println(quoteText); } /** * 获取面积的方法 * * @param area * @return */ public static double getHouseArea(String area) { double houseArea = RETURN_VALUE_INPUT_INVALID; if (area.strip().isBlank()) { houseArea = RETURN_VALUE_INPUT_IS_BLANK; // 面积的返回值设定为-2(无输入) } if ("EXIT".equalsIgnoreCase(area.strip())) { houseArea = RETURN_VALUE_INPUT_EXIT; } else { if (area.strip().matches(DOUBLE_REGEX)) { houseArea = Double.parseDouble(area); } } return houseArea; } /** * 获取装修档次装修套餐的菜单编号 * * @param selected * @return */ public static int getMenuNumber(String selected) { int menuNumber = RETURN_VALUE_INPUT_INVALID_MENU_NUMBER; if (selected.strip().isBlank()) { menuNumber = RETURN_VALUE_INPUT_IS_BLANK; } else { if (selected.strip().matches(MENU_INT_REGEX)) { // 正则判断selected的值为 0-3的数字,1-3为对应的套餐 int num = Integer.parseInt(selected); if (num > MENU_NUMBER_ZERO && num <= MENU_NUMBER_LUXURY) { menuNumber = switch (num) { case 1 -> MENU_NUMBER_ECONOMY; // 经济套餐的菜单编号 case 2 -> MENU_NUMBER_COMFORT; // 舒适套餐的菜单编号 case 3 -> MENU_NUMBER_LUXURY; // 豪华套餐的菜单编号 default -> RETURN_VALUE_INPUT_INVALID_MENU_NUMBER; }; } } } return menuNumber; } /** * 获取装修档次的描述 * * @param num * @return */ public static String getLevelDesc(int num) { return switch (num) { case 1 -> LEVEL_ECONOMY_DESC; case 2 -> LEVEL_COMFORT_DESC; case 3 -> LEVEL_LUXURY_DESC; default -> ""; }; } /** * 获取装修档次对应的单价 * * @param num * @return */ public static double getUnitPrice(int num) { return switch (num) { case 1 -> LEVEL_ECONOMY_UNIT_PRICE; // 经济套餐的菜单编号 case 2 -> LEVEL_COMFORT_UNIT_PRICE; // 舒适套餐的菜单编号 case 3 -> LEVEL_LUXURY_UNIT_PRICE; // 豪华套餐的菜单编号 default -> 0; }; } /** * 计算装修档次对应的总金额 * * @param houseArea * @param unitPrice * @return */ public static double calcTotalAmount(double houseArea, double unitPrice) { return houseArea * unitPrice; } /** * 获取订单编号 * * @return */ public static String getOrderNumber() { // 获取订单号码的编号,并产生订单号 // 订单编号:BJ2026-0001、BJ2026-0002 orderNumberCount++; String str = String.format(ORDER_NUMBER_FORMAT, orderNumberCount); return (PREFIX_ORDER_NO + str); } } ```
