Commit on 2025/04/18 周五 18:14:50.23

This commit is contained in:
zhangsan 2025-04-18 18:14:50 +08:00
parent 46f7212b9d
commit 63d3fc94e5
3 changed files with 329 additions and 27 deletions

View File

@ -88,7 +88,6 @@ ParentProject/
<module>MyProject2</module>
</modules>
</project>
```
3.修改子模块 `pom.xml` ,加上:

View File

@ -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

View File

@ -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<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 注解