diff --git a/自学/Maven.md b/自学/Maven.md index 8468890..d4980c9 100644 --- a/自学/Maven.md +++ b/自学/Maven.md @@ -88,7 +88,6 @@ ParentProject/ MyProject2 - ``` 3.修改子模块 `pom.xml` ,加上: diff --git a/自学/linux服务器.md b/自学/linux服务器.md index c102e68..8f21f45 100644 --- a/自学/linux服务器.md +++ b/自学/linux服务器.md @@ -782,6 +782,50 @@ services: ``` +### 调用API实现文件上传与下载 + +**登录:** + +```text +# 登录 → 获取 JWT token(原始字符串) +curl -X POST "yourdomain/api/login" \ + -H "Content-Type: application/json" \ + -d '{ + "username": "admin", + "password": "123456" + }' +响应: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…" //jwt令牌 +``` + +**上传:** + +```text +# 先把 token 保存到环境变量(假设你已经执行过 /api/login 并拿到了 JWT) +export TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…" + +# 定义要上传的本地文件和目标名称 +FILE_PATH="/path/to/local/photo.jpg" +OBJECT_NAME="photo.jpg" + +# 对远程路径做 URL 编码(保留斜杠) +REMOTE_PATH="store/${OBJECT_NAME}" +ENCODED_PATH=$(printf "%s" "${REMOTE_PATH}" \ + | jq -sRr @uri \ + | sed 's/%2F/\//g') + +# 发起上传请求(raw body 模式) +curl -v -X POST "https://yourdomain/api/resources/${ENCODED_PATH}?override=true" \ + -H "X-Auth: ${TOKEN}" \ + -H "Content-Type: image/jpeg" \ # 根据文件后缀改成 image/png 等 + --data-binary "@${FILE_PATH}" +``` + +**拼接下载url:** + +```text +String downloaded_url=yourdomain + "/api/raw/" + ${ENCODED_PATH}; +``` + ## Gitea @@ -998,6 +1042,19 @@ Joe主题:https://github.com/HaoOuBa/Joe [Joe再续前缘主题 - 搭建本站同款网站 - 易航博客](https://blog.yihang.info/archives/18.html) 自定义文章详情页的上方信息(如更新日期/文章字数第): + +`/typecho/usr/themes/Joe/functions.php`中定义art_count,统计字数(粗略)。 + +```text +原始 Markdown → + [1] 删除代码块/行内代码/图片/链接标记/标题列表标记/强调符号 → + [2] strip_tags() + html_entity_decode() → + [3] 正则保留 “文字+数字+标点”,去除其它 → + 结果用 mb_strlen() 得到最终字数 +``` + + + `typecho/usr/themes/Joe/module/single/batten.php` ```php diff --git a/自学/苍穹外卖.md b/自学/苍穹外卖.md index c26fdf5..dea48fe 100644 --- a/自学/苍穹外卖.md +++ b/自学/苍穹外卖.md @@ -242,7 +242,7 @@ server { 因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。 -- 统一入口解决跨域问题(无需后端配置 CORS)。 +- 统一入口**解决跨域**问题(无需后端配置 CORS)。 **nginx 反向代理的配置方式:** @@ -292,7 +292,14 @@ server{ -跨域问题: +windows下更新nginx配置:先cd到nginx安装目录 + +```bash +nginx -t +nginx -s reload +``` + + @@ -691,7 +698,114 @@ public class AliOssUtil { -### 加密算法 +#### 自己搭建FileBrowser存储 + +```java +public class FileBrowserUtil { + private String domain; + private String username; + private String password; + + /** + * —— 第一步:登录拿 token —— + * 调用 /api/login 接口,返回原始的 JWT token 字符串 + * curl -X POST "https://fshare.bitday.top/api/login" \ + * -H "Content-Type: application/json" \ + * -d '{ + * "username":"admin", + * "password":"asdf14789" + * }' + * 返回值: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9… + */ + + public String login() throws IOException { + String url = domain + "/api/login"; + // 创建 HttpClient 实例 + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + // 构造 POST 请求 + HttpPost httpPost = new HttpPost(url); + httpPost.setHeader("Content-Type", "application/json"); + + // 构造 JSON body + JSONObject json = new JSONObject(); + json.put("username", username); + json.put("password", password); + + // 设置请求体 + StringEntity requestEntity = new StringEntity(json.toString(), StandardCharsets.UTF_8); + requestEntity.setContentType("application/json"); + httpPost.setEntity(requestEntity); + + // 发送请求并处理响应 + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode >= 200 && statusCode < 300) { + HttpEntity respEntity = response.getEntity(); + String body = EntityUtils.toString(respEntity, StandardCharsets.UTF_8); + log.info("token:{}",body); + // 返回原始返回值(假设就是 JWT token 字符串) + return body; + } else { + log.error("Login failed, HTTP status code: " + statusCode); + return ""; + } + } + } + } + + /** + * —— 第二步:上传文件 —— + + * curl -v -X POST \ + + * "$DOMAIN/api/resources/$REMOTE_PATH?override=true" \ // 服务器上相对路径 + + * -H "X-Auth: $TOKEN" \ + + * -H "Content-Type: image/jpeg" \ // 根据文件类型替换 + + * --data-binary "@/path/to/local/photo.jpg" // 以 raw body 方式上传 + */ + + public String uploadAndGetUrl(byte[] fileBytes, String fileName) throws IOException { + // 1. 登录拿 token + String token = login(); + + // 2. 确定远端存储路径和编码(保留斜杠) + String remotePath = "store/" + fileName; + String encodedPath = URLEncoder.encode(remotePath, StandardCharsets.UTF_8.toString()) + .replace("%2F", "/"); + + // 3. 根据文件名猜 MIME 类型,fallback 到 application/octet-stream + String mimeType = URLConnection.guessContentTypeFromName(fileName); + if (mimeType == null) { + mimeType = "application/octet-stream"; + } + + // 4. 构造上传 URL + String uploadUrl = domain + "/api/resources/" + encodedPath + "?override=true"; + + // 5. 执行 Multipart upload + try (CloseableHttpClient client = HttpClients.createDefault()) { + HttpPost post = new HttpPost(uploadUrl); + post.setHeader("X-Auth", token); + post.setEntity(new ByteArrayEntity(fileBytes, ContentType.create(mimeType))); + try (CloseableHttpResponse resp = client.execute(post)) { + int status = resp.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8); + if (status < 200 || status >= 300) { + log.error("文件上传失败: HTTP {},{}", status, body); + return ""; + } + } + } + + // 6. 拼接 raw 下载链接并返回 + String downloadUrl = domain + "/api/raw/" + encodedPath; + log.info("文件下载链接:{}", downloadUrl); + return downloadUrl; + } +} +``` + + + +### 数据库密码加密 加密存储确保即使数据库泄露,攻击者也不能轻易获取用户原始密码。 @@ -743,29 +857,15 @@ boolean judge= passwordEncoder.matches(rawPassword, user.getPassword()); -### 新增员工的两个问题 +### BaseContext -**问题1:** - -录入的用户名已存,抛出的异常后没有处理。 - -法一:每次新增员工前查询一遍数据库,保证无重复username再插入。 - -法二:插入后系统报“Duplicate entry”再处理。 - -**推荐法二,因为发生异常的概率是很小的,每次新增前查询一遍数据库不划算。** - - - -**问题2:** - -如何获得当前登录的用户id? +**如何获得当前登录的用户id?** 方法:ThreadLocal ThreadLocal为**每个线程**提供**单独**一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。 -**每次请求代表一个线程**!!!注:请求可以先经过拦截器,再经过controller=>service=>mapper,都是在一个线程里。 +**每次请求代表一个线程**!!!注:请求可以先经过拦截器,再经过controller=>service=>mapper,都是在一个线程里。而即使同一个用户,先用两次请求/login、 /upload,它们也不处于同一线程中! ```java public class BaseContext { @@ -795,12 +895,85 @@ BaseContext.getCurrentId(); //service层中获取当前userId +### 全局异常处理 + +**新增员工时的问题** + +录入的用户名已存,抛出的异常后没有处理。 + +法一:每次新增员工前查询一遍数据库,保证无重复username再插入。 + +- [x] 法二:插入后系统报“Duplicate entry”再处理。 「乐观策略」减少不必要的查询,只有在冲突时才抛错。 + + +解决方法:定义全局异常处理器 + +```java +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * 捕获业务异常 + * @param ex + * @return + */ + @ExceptionHandler + public Result exceptionHandler(BaseException ex){ + log.error("异常信息:{}", ex.getMessage()); + return Result.error(ex.getMessage()); + } + @ExceptionHandler + public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){ + //Duplicate entry 'zhangsan' for key 'employee.idx_username' + String message = ex.getMessage(); + log.info(message); + if(message.contains("Duplicate entry")){ + String[] split = message.split(" "); + String username = split[2]; + String msg = username + MessageConstant.ALREADY_EXISTS; + return Result.error(msg); + }else{ + return Result.error(MessageConstant.UNKNOWN_ERROR); + } + } +} +``` + +`SQLIntegrityConstraintViolationException`用来捕获各种 **完整性约束冲突**,如唯一/主键约束冲突(向一个设置了 `UNIQUE` 或者 `PRIMARY KEY` 的字段重复插入相同的值。)。可以捕捉username重复异常。并以`Result.error(msg)`返回通用响应。 + + + +另外,自定义一个异常BaseException与若干个业务层异常, + +```java +public class BaseException extends RuntimeException { + public BaseException() { + } + public BaseException(String msg) { + super(msg); + } +} +``` + +![image-20250418124757147](https://pic.bitday.top/i/2025/04/18/kn2942-0.png) + +业务层抛异常: + +```java +throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED); +``` + +这样抛出异常之后可以被全局异常处理器 `exceptionHandler(BaseException ex)` 捕获。 + + + ### SpringMVC的消息转换器(处理日期) **Jackson** 是一个用于处理 **JSON 数据** 的流行 Java 库,主要用于: -1. **序列化**:将 Java 对象转换为 JSON 字符串(例如:`Java对象 → {"name":"Alice"}`)。 -2. **反序列化**:将 JSON 字符串解析为 Java 对象(例如:`{"name":"Alice"} → Java对象`)。 +1. **序列化**:将 Java 对象转换为 JSON 字符串(例如:`Java对象 → {"name":"Alice"}`)。Controller 返回值上带有 `@ResponseBody` 或者使用 `@RestController` +2. **反序列化**:将 JSON 字符串解析为 Java 对象(例如:`{"name":"Alice"} → Java对象`)。方法参数上标注了 `@RequestBody` **Spring Boot**默认集成了Jackson @@ -814,7 +987,7 @@ BaseContext.getCurrentId(); //service层中获取当前userId **2). 方式二(推荐 )** -在**WebMvcConfiguration**中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理 +在**WebMvcConfiguration**中扩展SpringMVC的消息转换器,统一对`LocalDateTime、LocalDate、LocalTime`进行格式处理 ```java /** @@ -869,9 +1042,14 @@ public class JacksonObjectMapper extends ObjectMapper { -### 修改员工信息(复用update方法) +### 数据库操作代码复用 -代码能复用尽量复用!在mapper类里定义一个**通用的update**接口,即mybatis操作数据库时修改员工信息都调用这个接口。**启用/禁用员工**可能只要修改status,**修改员工**可能大面积修改属性,在**mapper**类中定义一个通用的update方法,但是**controller层和service层**的函数命名可以不一样,以区分两种业务。 +为提高代码复用率,在 Mapper 层统一定义一个通用的 `update` 方法,利用 MyBatis 的动态 SQL,根据传入的 `Employee` 对象中非空字段生成对应的 `SET` 子句。这样: + +- **启用/禁用员工**:只需在业务层调用(如 `startOrStop`),传入带有 `id` 和 `status` 的 `Employee` 实例,底层自动只更新 `status` 字段。 +- **更新员工信息**:调用(如 `updateEmployee`)时,可传入包含多个属性的 `Employee` 实例,自动更新那些非空字段。 + +Controller 层和 Service 层的方法命名可根据不同业务场景进行区分,底层均复用同一个 update方法 @@ -907,6 +1085,64 @@ public class JacksonObjectMapper extends ObjectMapper { +### 操作多表时的规范操作 + +功能:实现批量删除套餐操作,只能删除'非起售中'的套餐,关联表有套餐表和套餐菜品表。 + +代码1: + +```java +@Transactional + public void deleteBatch(Long[] ids) { + for(Long id:ids){ + Setmeal setmeal = setmealMapper.getById(id); + if(StatusConstant.ENABLE == setmeal.getStatus()){ + //起售中的套餐不能删除 + throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE); + } + else{ + Long setmealId=id; + setmealMapper.deleteById(id); + setmeal_dishMapper.deleteByDishId(id); + } + } + } +``` + +代码2: + +```java +@Transactional + public void deleteBatch(List ids) { + ids.forEach(id -> { + Setmeal setmeal = setmealMapper.getById(id); + if (StatusConstant.ENABLE == setmeal.getStatus()) { + //起售中的套餐不能删除 + throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE); + } + }); + + ids.forEach(setmealId -> { + //删除套餐表中的数据 + setmealMapper.deleteById(setmealId); + //删除套餐菜品关系表中的数据 + setmealDishMapper.deleteBySetmealId(setmealId); + }); + } +``` + +代码2更好,因为: + +1.把「验证」逻辑和「删除」逻辑分成了两段,职责更单一,读代码的时候一目了然 + +2.避免不必要的删除操作,第一轮只做 `getById` 校验,碰到 `起售中` 马上抛异常,**从不执行**任何删除 SQL,效率更高。 + + + +`@Transactional` 最典型的场景就是:**在同一个业务方法里要执行多条数据库操作(增删改),而且这些操作必须保证“要么都成功、要么都失败”** 时,用它来把这些 SQL 语句包裹在同一个事务里,遇到运行时异常就回滚,避免出现“删到一半、中途抛错”导致的数据不一致。也就是说,同时操作多表时,都在方法上加下这个注解! + + + ### 公共字段自动填充——AOP编程 在数据库操作中,通常需要为某些公共字段(如创建时间、更新时间等)自动赋值。采用AOP: @@ -930,7 +1166,17 @@ public class JacksonObjectMapper extends ObjectMapper { 2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值 -3). 在 Mapper 的方法上加入 AutoFill 注解 +```text +客户端 → Service → Mapper接口方法(带@AutoFill) + ↓ 切面触发 Before 通知(AutoFillAspect.autoFill) +[1] 读取注解,确定 INSERT/UPDATE +[2] 从 BaseContext 拿到 currentId +[3] 反射调用 entity.setXxx() + ↓ 切面执行完毕,回到原方法 +Mapper 执行动态 SQL,将已填充的字段写入数据库 +``` + +3). 在 需要统一填充的Mapper 的方法上加入 AutoFill 注解