From 63d3fc94e53a0e87477b64cba562844875084e22 Mon Sep 17 00:00:00 2001
From: zhangsan <646228430@qq.com>
Date: Fri, 18 Apr 2025 18:14:50 +0800
Subject: [PATCH] =?UTF-8?q?Commit=20on=202025/04/18=20=E5=91=A8=E4=BA=94?=
=?UTF-8?q?=2018:14:50.23?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
自学/Maven.md | 1 -
自学/linux服务器.md | 57 +++++++++
自学/苍穹外卖.md | 298 ++++++++++++++++++++++++++++++++++++++++----
3 files changed, 329 insertions(+), 27 deletions(-)
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);
+ }
+}
+```
+
+
+
+业务层抛异常:
+
+```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 注解