Commit on 2025/04/18 周五 18:14:50.23
This commit is contained in:
parent
46f7212b9d
commit
63d3fc94e5
@ -88,7 +88,6 @@ ParentProject/
|
||||
<module>MyProject2</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
||||
```
|
||||
|
||||
3.修改子模块 `pom.xml` ,加上:
|
||||
|
@ -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
|
||||
|
298
自学/苍穹外卖.md
298
自学/苍穹外卖.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<Long> 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 注解
|
||||
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user