From 2b7da7a7850f0237f2e333cb078b3595ebdc69ae Mon Sep 17 00:00:00 2001 From: zhangsan <646228430@qq.com> Date: Fri, 8 Aug 2025 22:19:05 +0800 Subject: [PATCH] =?UTF-8?q?Commit=20on=202025/08/08=20=E5=91=A8=E4=BA=94?= =?UTF-8?q?=2022:19:05.76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 后端学习/JAVA面试题.md | 2 +- 后端学习/JavaWeb——后端.md | 80 +++- 后端学习/Java笔记本.md | 2 - 后端学习/微服务.md | 2 +- 杂项/mermaid画图.md | 79 ++++ 科研/草稿.md | 278 +++-------- 论文/陈茂森论文.md | 4 +- 论文/高飞论文.md | 110 ++++- 项目/拼团交易系统.md | 966 ++++++++++++++++++++++++-------------- 9 files changed, 906 insertions(+), 617 deletions(-) diff --git a/后端学习/JAVA面试题.md b/后端学习/JAVA面试题.md index 4d01b6b..476c9f4 100644 --- a/后端学习/JAVA面试题.md +++ b/后端学习/JAVA面试题.md @@ -92,7 +92,7 @@ if (V == A) { -如何多线程循环打印1-100数字? +## 如何多线程循环打印1-100数字? ```java public class AlternatePrint { diff --git a/后端学习/JavaWeb——后端.md b/后端学习/JavaWeb——后端.md index cd7eec1..f132b92 100644 --- a/后端学习/JavaWeb——后端.md +++ b/后端学习/JavaWeb——后端.md @@ -2344,47 +2344,75 @@ public class LoggingAspect { -#### annotation +#### @annotation -那么如果我们要匹配多个无规则的方法,比如:list()和 delete()这**两个**方法。我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。 +在实际项目中,有时我们需要对**多个方法**(比如 `list()` 和 `delete()`)进行统一拦截,这些方法可能**命名无规律**、无法用 `execution()` 之类的表达式轻松匹配。 + +这时就可以: + +- 给这些方法**统一加一个自定义注解**; +- 在 AOP 切面里用 `@annotation(...)` 表达式匹配这些方法; +- 这样写的切入点既简单又易维护。 实现步骤: -1. **新建anno包,在这个包下**编写自定义注解 +① 定义注解 ```java -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import java.lang.annotation.*; -// 定义注解 -@Retention(RetentionPolicy.RUNTIME) // 定义注解的生命周期 -@Target(ElementType.METHOD) // 定义注解可以应用的Java元素类型 +@Retention(RetentionPolicy.RUNTIME) // 运行时可反射获取 +@Target(ElementType.METHOD) // 只能标记方法 public @interface MyLog { - // 定义注解的元素(属性) - String description() default "This is a default description"; - int value() default 0; -} - -``` - -2. 在业务类要做为连接点的**方法上添加**自定义注解 - -```java -@MyLog //自定义注解(表示:当前方法属于目标方法) -public void delete(Integer id) { - //1. 删除部门 - deptMapper.delete(id); + String description() default "default description"; // 描述信息 + int value() default 0; // 额外参数 } ``` -3. AOP切面类上使用类似如下的切面表达式: +`@Retention(RUNTIME)` 保证运行时可以通过反射拿到注解。 + +`@Target(METHOD)` 限制只能用于方法。 + +②在业务方法上加注解 ```java -@Before("@annotation(edu.whut.anno.MyLog)") +@Service +public class DeptService { + + @MyLog(description = "删除部门", value = 1) + public void delete(Integer id) { + deptMapper.delete(id); + } + + @MyLog(description = "查询部门列表") + public List list() { + return deptMapper.findAll(); + } +} ``` +③定义切面 + +```java +@Aspect +@Component +public class MyLogAspect { + @Before("@annotation(myLog)") // 绑定注解对象到参数 + public void before(JoinPoint joinPoint, MyLog myLog) { + String methodName = joinPoint.getSignature().getName(); + System.out.println("方法:" + methodName); + System.out.println("注解描述:" + myLog.description()); + System.out.println("注解值:" + myLog.value()); + } +} +``` + +`@annotation(myLog)` 表示匹配所有带 `@MyLog` 的方法; + +**`myLog` 参数** 会直接被赋值为该方法上的注解实例,可以直接读取注解里的属性值; + +**不需要手动反射**去找注解,Spring AOP 自动完成了注解解析和注入。 + ### 连接点JoinPoint diff --git a/后端学习/Java笔记本.md b/后端学习/Java笔记本.md index cd6dc85..b25e3d3 100644 --- a/后端学习/Java笔记本.md +++ b/后端学习/Java笔记本.md @@ -1986,8 +1986,6 @@ public class MyClass { } ``` - - ```java import java.lang.reflect.Method; diff --git a/后端学习/微服务.md b/后端学习/微服务.md index 5ca43c0..039fc32 100644 --- a/后端学习/微服务.md +++ b/后端学习/微服务.md @@ -406,7 +406,7 @@ More options->Add VM options -> **-Dserver.port=xxxx** 2.需要.env文件,配置和数据库的连接信息: -```text +```.env PREFER_HOST_MODE=hostname MODE=standalone SPRING_DATASOURCE_PLATFORM=mysql diff --git a/杂项/mermaid画图.md b/杂项/mermaid画图.md index 5815658..66ccfb1 100644 --- a/杂项/mermaid画图.md +++ b/杂项/mermaid画图.md @@ -150,3 +150,82 @@ flowchart LR ``` + + + + +```mermaid +sequenceDiagram + participant A as 启动时 + participant B as BeanPostProcessor + participant C as 管理后台 + participant D as Redis Pub/Sub + participant E as RTopic listener + participant F as Bean 字段热更新 + + A->>B: 扫描 @DCCValue 标注的字段 + B->>B: 写入默认值 / 读取 Redis + B->>B: 注入字段值 + B->>B: 缓存 key→Bean 映射 + A->>A: Bean 初始化完成 + + C->>D: publish("myKey,newVal") + D->>E: 订阅频道 "dcc_update" + E->>E: 收到消息,更新 Redis + E->>E: 从 Map 找到 Bean + E->>E: 反射注入新值到字段 + E->>F: Bean 字段热更新完成 + +``` + + + + + +```mermaid +sequenceDiagram + participant A as 后台/系统 + participant B as Redis Pub/Sub + participant C as DCC监听器 + participant D as Redis数据库 + participant E as 反射更新字段 + participant F as Bean实例 + + A->>B: 发布消息 ("cutRange:50") + B->>D: 将消息 "cutRange:50" 写入 Redis + B->>C: 触发订阅者接收消息 + C->>D: 更新 Redis 中的 "cutRange" 配置值 + C->>F: 根据映射找到对应的 Bean + C->>E: 通过反射更新 Bean 中的字段 + E->>C: 更新成功,字段值被同步 + C->>A: 配置变更更新完成 + +``` + + + +```mermaid +classDiagram + class Client + class Context { + - Strategy strategy + + execute() + } + class Strategy { + <> + + execute() + } + class ConcreteStrategyA { + + execute() + } + class ConcreteStrategyB { + + execute() + } + + Client --> Context + Context --> Strategy + Strategy <|.. ConcreteStrategyA + Strategy <|.. ConcreteStrategyB + +``` + diff --git a/科研/草稿.md b/科研/草稿.md index 31047b9..e5bc5cb 100644 --- a/科研/草稿.md +++ b/科研/草稿.md @@ -1,230 +1,62 @@ +是的,你理解的方向是对的,不过我们可以更精确地说清楚它的含义: +------ -### 定理2 -多智能体随机网络矩阵奇异值信号系统具有线性特征。 +## 1. 它从哪里来 -#### 证明 -根据定理1,奇异值序列$\sigma_{\tilde{\kappa}}(A_t)$服从高斯分布$\mathcal{N}(m_{\tilde{\kappa}}, 2\sigma_{\tilde{\kappa}}^2)$,其协方差结构满足: +你给的公式 + +x^k=x^k−+Kk(zk−Hx^k−)\hat{\mathbf{x}}_k = \hat{\mathbf{x}}_k^- + \mathbf{K}_k \left(\mathbf{z}_k - \mathbf{H} \hat{\mathbf{x}}_k^- \right) + +中, + +ek:=zk−Hx^k−\mathbf{e}_k := \mathbf{z}_k - \mathbf{H} \hat{\mathbf{x}}_k^- + +是**观测创新(innovation)**,反映了观测值与预测值的差异。 + 在逐维情形下, + +x^i,k=x^i,k−+Ki,kei,k.\hat{x}_{i,k} = \hat{x}_{i,k}^- + K_{i,k} e_{i,k}. + +如果 $K_{i,k}$ 是确定的,那么更新量 + +Δx^i,k=Ki,kei,k\Delta \hat{x}_{i,k} = K_{i,k} e_{i,k} + +的波动只由 $e_{i,k}$ 决定。 + +但实际中,你的 $K_{i,k}$ 不是已知的常数,而是由 $Q_i,R_i$ 的估计不确定性决定的——这就是我们推导的 $\theta_{i,\min},\theta_{i,\max}$ 及其对应的 $K_{i,\min},K_{i,\max}$。 + +------ + +## 2. $|\xi|_\infty$ 表示的含义 + +公式 + +∥ξ∥∞≤max⁡i{θi,max⁡2−θi,min⁡2(1+θi,min⁡2)(1+θi,max⁡2)}⋅∥μ∥∞\|\xi\|_\infty \le \max_i\left\{ \frac{\theta_{i,\max}^2-\theta_{i,\min}^2}{(1+\theta_{i,\min}^2)(1+\theta_{i,\max}^2)} \right\} \cdot \|\mu\|_\infty + +其实是在说: + +- **$\frac{\theta_{i,\max}^2-\theta_{i,\min}^2}{(1+\theta_{i,\min}^2)(1+\theta_{i,\max}^2)}$** 是**增益 $K_{i,k}$ 的最大可能变化幅度**(上界与下界之差)。 +- **$\mu_i$** 是历史上**创新 $e_{i,k}$ 的平均绝对值**($E|e_{i,k}|$),代表“输入量”的典型大小。 +- 两者相乘,就是**因 $K_{i,k}$ 不确定性带来的 $\hat{x}_{i,k}$ 更新项波动的绝对值上界**。 + +取 $\infty$ 范数,就是取所有维度中最大的这个影响: + +∥ξ∥∞≈max⁡i(增益变化幅度)×max⁡i(创新大小).\|\xi\|_\infty \approx \max_i \text{(增益变化幅度)} \times \max_i \text{(创新大小)}. + +------ + +## 3. 直观解释 + +可以把它理解成: + +> **“当我们对 $Q_i,R_i$ 的估计不确定时,卡尔曼增益 $K_i$ 会有一个可能的变化区间,这会导致更新量 $K_i e_i$ 出现额外波动。$|\xi|_\infty$ 给出了这种波动的最坏情况下的绝对值上界。”** + +换句话说,它衡量的正是你问的 $$ -\gamma_{\tilde{\kappa}}(h) = 2\sigma_{\tilde{\kappa}}^2\delta_h^0 +Kk(zk−Hx^k−)\mathbf{K}_k \left(\mathbf{z}_k - \mathbf{H} \hat{\mathbf{x}}_k^- \right) $$ +这一项的**不确定性幅度**,但这里是**最坏情况下的最大绝对偏差**(sup-norm),而不是平均波动。 -定义中心化变量: -$$ -\tilde{\sigma}_t = \sigma_{\tilde{\kappa}}(A_t) - m_{\tilde{\kappa}} -$$ -可表示为: -$$ -\tilde{\sigma}_t = \sqrt{2}\sigma_{\tilde{\kappa}}\varepsilon_t, \quad \varepsilon_t \overset{i.i.d.}{\sim} \mathcal{N}(0,1) -$$ +------ -#### 线性系统验证 -该系统为MA(0)过程,系统增益$h_0 = \sqrt{2}\sigma_{\tilde{\kappa}}$,满足: -1. **齐次性**: - $$a\tilde{\sigma}_t = h_0(a\varepsilon_t)$$ -2. **叠加性**: - $$\tilde{\sigma}_t^{(1)} + \tilde{\sigma}_t^{(2)} = h_0(\varepsilon_t^{(1)} + \varepsilon_t^{(2)})$$ - -#### 结论 -奇异值序列的完整表示: -$$ -\sigma_{\tilde{\kappa}}(A_t) = m_{\tilde{\kappa}} + h_0\varepsilon_t -$$ -其中: -- $m_{\tilde{\kappa}}$为稳态偏置项 -- $h_0\varepsilon_t$为线性系统响应 - -根据线性系统定义(需引用文献),同时满足齐次性与可加性即构成线性系统,故得证。 - - - - - - - - - - - - - - - ---- - -### ② 定理2修订(线性系统特征) - -#### 原MA(0)情形回顾 - -当$\gamma_k(h)=2\sigma_k^2\delta_h$时, -$$ -\tilde{\sigma}_t=\sigma_k(A_t)-m_k=\sqrt{2}\sigma_k\varepsilon_t, \quad \varepsilon_t \overset{i.i.d.}{\sim} \mathcal{N}(0,1) -$$ - -#### 新协方差结构下的表示 - -当$\gamma_k(h)=C_h$(允许$C_h\neq0$),根据Wiener-Kolmogorov表示定理: -$$ -\tilde{\sigma}_t=\sum_{h=-\infty}^{+\infty} b_h w_{t-h} \tag{1} -$$ -其中$\{b_h\}\in\ell^2$满足: -$$ -\gamma_k(h)=\sum_{\ell=-\infty}^{+\infty} b_\ell b_{\ell+h} \tag{2} -$$ - -#### 线性系统验证 - -设系统传递函数$H(z)=\sum_h b_h z^{-h}$: - -1. **齐次性** - $$ - a\tilde{\sigma}_t=a\sum_h b_h w_{t-h}=\sum_h b_h (a w_{t-h})=H(z)\{a w_t\} - $$ - -2. **叠加性** - $$ - \tilde{\sigma}_t^{(1)}+\tilde{\sigma}_t^{(2)}=\sum_h b_h(w_{t-h}^{(1)}+w_{t-h}^{(2)})=H(z)\{w_t^{(1)}+w_t^{(2)}\} - $$ - -故$\{\sigma_k(A_t)\}$仍是LTI系统输出,但系统响应$\{b_h\}$需通过(2)式确定。 - ---- - -### 性质对比 - -| 性质 | $\gamma_k(h)=2\sigma_k^2\delta_h$ | $\gamma_k(h)=C_h$ | -| -------- | --------------------------------- | -------------------------------- | -| 宽平稳 | ✅ | ✅ | -| 白噪声 | ✅ | ❌ | -| 系统类型 | MA(0) | 通用LTI(可能MA($\infty$)) | -| 谱密度 | $S(f)=2\sigma_k^2$ | $S(f)=\sum_h C_h e^{-j2\pi f h}$ | - - - - - - - -### 随机网络稳态奇异值的平稳性证明 - -#### 1. 稳态奇异值分布特性 - -当随机网络进入稳态后,其矩阵序列$\{A_t\}$的任意奇异值$\sigma_k(A_t)$服从高斯分布: -$$ -\sigma_k(A_t) \sim \mathcal{N}(m_k, \gamma_k(0)) -$$ -其中参数满足: - -- **均值**:$m_k = (N-1)\mu_k + v_k + \frac{\sigma_k^2}{\mu_k}$ - ($N$为网络规模,$\mu_k,v_k,\sigma_k$为网络参数) -- **方差**:$\gamma_k(0) = 2\sigma_k^2$ - -#### 2. 宽平稳性验证 - -对任意时刻$t$: - -1. **均值稳定性**: - $$ - \mathbb{E}[\sigma_k(A_t)] = m_k \quad \text{(常数)} - $$ - -2. **协方差结构**: - - - 当$h=0$时: - $$ - \text{Cov}(\sigma_k(A_t), \sigma_k(A_t)) = \gamma_k(0) - $$ - - - 当$h \neq 0$时: - $$ - \text{Cov}(\sigma_k(A_t), \sigma_k(A_{t+h})) = \gamma_k(h)=0 - $$ - (由稳态下矩阵的独立性保证) - -#### 3. 结论 - -自协方差函数$\gamma_k(h)$仅依赖于时滞$h$,因此奇异值信号序列$\{\sigma_k(A_t)\}$满足宽平稳过程的定义。 - ---- - -**注**:本证明基于以下假设: - -1. 网络规模$N$足够大,使得高斯逼近有效 -2. 稳态下矩阵序列$\{A_t\}$具有独立性 - - - -### 定理2 -多智能体随机网络矩阵奇异值信号系统具有线性特征。 - -#### 证明 -根据定理1,奇异值序列$\sigma_{\tilde{\kappa}}(A_t)$服从高斯分布$\mathcal{N}(m_{\tilde{\kappa}}, 2\sigma_{\tilde{\kappa}}^2)$,其协方差结构满足: -$$ -\gamma_{\tilde{\kappa}}(h) = 2\sigma_{\tilde{\kappa}}^2\delta_h^0 -$$ - -定义中心化变量: -$$ -\tilde{\sigma}_t = \sigma_{\tilde{\kappa}}(A_t) - m_{\tilde{\kappa}} -$$ -可表示为: -$$ -\tilde{\sigma}_t = \sqrt{2}\sigma_{\tilde{\kappa}}\varepsilon_t, \quad \varepsilon_t \overset{i.i.d.}{\sim} \mathcal{N}(0,1) -$$ - -#### 线性系统验证 -该系统为MA(0)过程,系统增益$h_0 = \sqrt{2}\sigma_{\tilde{\kappa}}$,满足: -1. **齐次性**: - $$a\tilde{\sigma}_t = h_0(a\varepsilon_t)$$ -2. **叠加性**: - $$\tilde{\sigma}_t^{(1)} + \tilde{\sigma}_t^{(2)} = h_0(\varepsilon_t^{(1)} + \varepsilon_t^{(2)})$$ - -#### 结论 -奇异值序列的完整表示: -$$ -\sigma_{\tilde{\kappa}}(A_t) = m_{\tilde{\kappa}} + h_0\varepsilon_t -$$ -其中: -- $m_{\tilde{\kappa}}$为稳态偏置项 -- $h_0\varepsilon_t$为线性系统响应 - -根据线性系统定义(需引用文献),同时满足齐次性与可加性即构成线性系统,故得证。 - - - - - - - - - - - - - - - -……由协方差结构 γ_k(h)=2σ_k^2δ_h^0 可知,中心化变量 -$$ -\tilde σ_t = σ_k(A_t)-m_k,\qquad -\mathbb E[\tilde σ_t]=0,\; \mathrm{Cov}(\tilde σ_t,\tilde σ_{t+h})= -2σ_k^{2}\delta_h^{0}. -$$ - -**根据 Wold 分解定理①**,任何零均值、纯非确定性的宽平稳过程都可以唯一表示为 - -$$ -\tilde σ_t=\sum_{j=0}^{\infty}ψ_j\;ε_{t-j}, -\qquad ε_t\stackrel{i.i.d.}{\sim}\mathcal N(0,1),\ -\sum_{j=0}^{\infty}|ψ_j|^2<\infty. -$$ - -而在本情形下 $\gamma_k(h)=0\,(h\neq 0)$,因此 -$$ -ψ_0=\sqrt{2}\,σ_k,\quad ψ_j=0\;(j\ge 1), -$$ -退化为一个 **MA(0)** 过程: -$$ -\boxed{\;\tilde σ_t=\sqrt{2}\,σ_k\,ε_t\;} -$$ -…… +我可以帮你画一张图,把 **$K_i$ 区间 × 创新幅度 → 更新量波动范围** 的关系可视化,让你直观看出 $\xi$ 是怎么来的。这样你一看就能明白它在公式里扮演的角色。你要我画吗? diff --git a/论文/陈茂森论文.md b/论文/陈茂森论文.md index ea05362..08f9ac6 100644 --- a/论文/陈茂森论文.md +++ b/论文/陈茂森论文.md @@ -260,7 +260,7 @@ $$ #### 构造李雅普诺夫函数 -对一维系统 $\dot e=-ce$($c>0$),自然选取 +对一维系统 $\dot e=-ce(c>0)$,自然选取 $$ V(e)=e^2 $$ @@ -269,6 +269,8 @@ $$ - $V(e)>0$ 当且仅当 $e\neq0$; - 平衡点 $e=0$ 时,$V(0)=0$。 + + #### 计算 $V$ 的时间导数 对 $V$ 关于时间求导: diff --git a/论文/高飞论文.md b/论文/高飞论文.md index 251ff60..f34c112 100644 --- a/论文/高飞论文.md +++ b/论文/高飞论文.md @@ -409,11 +409,117 @@ $$ +# 向量情形(对角 $Q,R$,$F=I,\ H=I$)的区间估计与增益上界 + +**假设.** +$Q=\mathrm{diag}(Q_i),\ R=\mathrm{diag}(R_i)$,且 $F=I,\ H=I$。 +由于 $F$ 为单位矩阵,预测协方差递推为 +$$ +P_k^- = P_{k-1} + Q +$$ +并保持对角结构,因此各维度**相互独立**,可逐维应用标量推导(标量情形就是单个奇异值,完整向量情形就是若干个奇异值并行计算)。 + +--- + +## 1. 噪声样本与方差(逐维) + +记第 $i$ 维过程噪声与观测噪声的样本为 +$$ +\{w_{i,\ell}\}_{\ell=1}^{N_{w,i}},\quad \{v_{i,\ell}\}_{\ell=1}^{N_{v,i}} +$$ +其样本均值与样本方差为 +$$ +\bar w_i=\frac{1}{N_{w,i}}\sum_{\ell=1}^{N_{w,i}} w_{i,\ell},\quad s_{w,i}^2=\frac{1}{N_{w,i}-1}\sum_{\ell=1}^{N_{w,i}}(w_{i,\ell}-\bar w_i)^2, +$$ + +$$ +\bar v_i=\frac{1}{N_{v,i}}\sum_{\ell=1}^{N_{v,i}} v_{i,\ell},\quad s_{v,i}^2=\frac{1}{N_{v,i}-1}\sum_{\ell=1}^{N_{v,i}}(v_{i,\ell}-\bar v_i)^2. +$$ + +高斯假设下,真实方差满足 +$$ +w_{i,\ell}\sim\mathcal N(0,Q_i),\qquad v_{i,\ell}\sim\mathcal N(0,R_i). +$$ + +> 若各维度样本数一致,可写作 $N_{w,i}\equiv N_w,\ N_{v,i}\equiv N_v$ 简化记号。 + +--- + +## 2. 方差比的 $F$ 分布区间估计(逐维) + +对于每个维度 $i$,统计量为 +$$ +F_i=\frac{(s_{w,i}^2/Q_i)}{(s_{v,i}^2/R_i)} = \frac{s_{w,i}^2}{s_{v,i}^2}\cdot\frac{R_i}{Q_i} \sim F(N_{w,i}-1,\,N_{v,i}-1). +$$ +给定置信度 $1-\alpha$,定义 +$$ +F_L=F_{\alpha/2}(N_{w,i}-1,\,N_{v,i}-1),\quad F_U=F_{1-\alpha/2}(N_{w,i}-1,\,N_{v,i}-1), +$$ +则 +$$ +P\!\left\{ F_L\le F_i\le F_U \right\}=1-\alpha +$$ +等价于 +$$ +P\!\left\{ F_L\,\frac{s_{v,i}^2}{s_{w,i}^2}\le \frac{R_i}{Q_i}\le F_U\,\frac{s_{v,i}^2}{s_{w,i}^2} \right\}=1-\alpha. +$$ +记 +$$ +\theta_{i,\min}=\sqrt{F_L\,\frac{s_{v,i}^2}{s_{w,i}^2}},\qquad \theta_{i,\max}=\sqrt{F_U\,\frac{s_{v,i}^2}{s_{w,i}^2}}, +$$ +则 +$$ +\boxed{\ \frac{R_i}{Q_i}\in\big[\theta_{i,\min}^2,\ \theta_{i,\max}^2\big]\ }. +$$ + +--- + +## 3. 卡尔曼增益与误差上界(逐维) + +由 $F=I,\ H=I$,第 $i$ 维的预测协方差为 +$$ +P_i^- = P_{i,\text{prev}} + Q_i, +$$ +其中 $P_{i,\text{prev}}$ 是上一步更新后的 $i$ 维协方差。 + +卡尔曼增益为 +$$ +K_i=\frac{P_i^-}{P_i^-+R_i} = \frac{1}{1+\rho_i},\quad \rho_i:=\frac{R_i}{P_i^-}. +$$ +若采用近似(忽略 $P_{i,\text{prev}}$ 的波动),则 +$$ +\rho_i\in\big[\theta_{i,\min}^2,\ \theta_{i,\max}^2\big], +$$ +得到**逐维卡尔曼增益上下界** +$$ +\boxed{\; K_{i,\max}=\frac{1}{1+\theta_{i,\min}^2},\quad K_{i,\min}=\frac{1}{1+\theta_{i,\max}^2}\; } +$$ +令 $\mu_{i} := E|\sigma_{i} - \sigma_{i}^{\prime}|$ 为历史数据估计的第 $i$ 维预测误差均值,则绝对误差上界为 +$$ +\boxed{\; \xi_i=(K_{i,\max}-K_{i,\min})\,\mu_i = \frac{\theta_{i,\max}^2-\theta_{i,\min}^2}{(1+\theta_{i,\min}^2)(1+\theta_{i,\max}^2)}\ \mu_i\; } +$$ +整体界可取 +$$ +\|\xi\|_\infty \le \max_i\!\left\{\frac{\theta_{i,\max}^2-\theta_{i,\min}^2}{(1+\theta_{i,\min}^2)(1+\theta_{i,\max}^2)}\right\} \cdot \|\mu\|_\infty, +$$ +或类似的 $\ell_2$ 范数界。 + +> **更严谨的变体(可选)**:若显式纳入 $Q_i$ 的不确定性,先用卡方区间给出 $Q_{i,L},Q_{i,U}$,再由 + +$$ +> \rho_i=\frac{(R_i/Q_i)\,Q_i}{P_{i,\text{prev}}+Q_i} +> $$ +> 的单调性得到 +> +$$ - - +> $$ +> \rho_{i,\min}=\frac{\theta_{i,\min}^2 Q_{i,L}}{P_{i,\text{prev}}+Q_{i,L}},\quad \rho_{i,\max}=\frac{\theta_{i,\max}^2 Q_{i,U}}{P_{i,\text{prev}}+Q_{i,U}}, +> $$ +> +> ## 基于时空特征的节点位置预测 diff --git a/项目/拼团交易系统.md b/项目/拼团交易系统.md index 319d626..63de842 100644 --- a/项目/拼团交易系统.md +++ b/项目/拼团交易系统.md @@ -508,49 +508,11 @@ flowchart LR -## 收获 - -### 实体对象 - -实体是指具有唯一标识的业务对象。 - -在 DDD 分层里,**Domain Entity ≠ 数据库 PO**。 -在 `edu.whut.domain.*.model.entity` 包下放的是**纯粹的业务对象**,它们只表达业务语义(团队 ID、活动时间、优惠金额……),对「数据持久化细节」保持**无感知**。因此它们看起来“字段不全”是正常的: - -- 它们不会带 `@TableName` / `@TableId` 等 MyBatis-Plus 注解; -- 也不会出现数据库的技术字段(`id`、`create_time`、`update_time`、`status` 等); -- 只保留聚合根真正**需要的**业务属性与行为。 - -```java -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class PayActivityEntity { - - /** 拼单组队ID */ - private String teamId; - /** 活动ID */ - private Long activityId; - /** 活动名称 */ - private String activityName; - /** 拼团开始时间 */ - private Date startTime; - /** 拼团结束时间 */ - private Date endTime; - /** 目标数量 */ - private Integer targetCount; - -} -``` - -这个也是实体对象,因为多个字段的组合:teamId和activityId能唯一标识这个实体。 - - +## 设计模式 ### 单例模式 -**懒汉** +#### 懒汉 ```java public class LazySingleton { @@ -569,10 +531,23 @@ public class LazySingleton { return instance; } } - ``` -**饿汉** +第一次检查:防止重复实例化、以及进行synchronized同步块。 + +第二次检查:防止有多个线程同时通过第一次检查,然后依次进入同步块后,创建N个实例。 + +volatile:防止指令重排序,instance = new LazySingleton(); 正确顺序是: + +**1.分配内存** + +**2.调用构造函数,初始化对象** + +**3.把引用赋给 `instance`** + + + +#### 饿汉 ```java public class EagerSingleton { @@ -605,7 +580,10 @@ public class EagerSingleton { ### 模板方法 **核心思想**: -在抽象父类中定义**算法骨架**(固定执行顺序),把某些可变步骤留给子类重写;调用方只用模板方法,保证流程一致。 + +在抽象父类中定义**算法骨架**(固定**执行顺序**),把某些可变步骤留给子类重写;调用方只用模板方法,保证流程一致。 + +如果仅仅是把重复的方法抽取成公共函数,不叫模板方法!模板方法要设计算法骨架!!! ```text Client ───▶ AbstractClass @@ -688,11 +666,151 @@ public class Demo { +### 策略模式 + +**核心思想**: + +将可以互换的算法或行为抽象为独立的策略类,运行时由**上下文类(Context)**选择合适的策略对象去执行。调用方(Client)只依赖统一的接口,不关心具体实现。 + +```text +┌───────────────┐ +│ Client │ +└─────▲─────────┘ + │ has-a +┌─────┴─────────┐ implements +│ Context │────────────┐ ┌──────────────┐ +│ (使用者) │ strategy └─▶│ Strategy A │ +└───────────────┘ ├──────────────┤ + │ Strategy B │ + └──────────────┘ +``` + +```java +// 策略接口 +public interface PaymentStrategy { + void pay(int amount); +} + +// 策略A:微信支付 +@Service("wechat") +public class WechatPay implements PaymentStrategy { + public void pay(int amount) { + System.out.println("使用微信支付 " + amount + " 元"); + } +} + +// 策略B:支付宝支付 +@Service("alipay") +public class Alipay implements PaymentStrategy { + public void pay(int amount) { + System.out.println("使用支付宝支付 " + amount + " 元"); + } +} + +// 上下文类 +public class PaymentContext { + private PaymentStrategy strategy; + + public PaymentContext(PaymentStrategy strategy) { + this.strategy = strategy; + } + + public void execute(int amount) { + strategy.pay(amount); + } +} + +// 调用方 +public class Main { + public static void main(String[] args) { + PaymentContext ctx = new PaymentContext(new WechatPay()); + ctx.execute(100); + + ctx = new PaymentContext(new Alipay()); + ctx.execute(200); + } +} +``` + +下面有更优雅的策略选择方式! + + + +### Spring集合自动注入 + +在策略、工厂、插件等模式中,经常需要维护**“策略名 → 策略对象”**的映射。Spring 可以通过 `Map` **一次性注入**所有实现类。 + +```java +@Resource +private Map discountCalculateServiceMap; +``` + +**字段类型**:`Map` + +- key—— **Bean 的名字** + - 默认是类名首字母小写 (`mjCalculateService`) + - 或者你在实现类上显式写的 `@Service("MJ")` +- **value** —— 那个实现类对应的**实例** +- **Spring 机制**: + 1. 启动时扫描所有实现 `IDiscountCalculateService` 的 Bean。 + 2. 把它们按 “BeanName → Bean 实例” 的映射注入到这张 `Map` 里。 + 3. 你一次性就拿到了“策略字典”。 + +**示例:** + +```java +// 上下文类:自动注入所有策略 Bean +@Component +@RequiredArgsConstructor +public class PaymentContext { + + // key 为 Bean 名(如 "wechat"、"alipay"),value 为策略实例 + private final Map paymentStrategyMap; + + public void pay(String strategyKey, int amount) { + PaymentStrategy strategy = paymentStrategyMap.get(strategyKey); + if (strategy == null) { + throw new IllegalArgumentException("无匹配支付方式: " + strategyKey); + } + strategy.pay(amount); + } +} + +// 调用方示例 +@Component +@RequiredArgsConstructor +public class PaymentService { + + private final PaymentContext paymentContext; + + public void process() { + paymentContext.pay("wechat", 100); // 输出:使用微信支付 100 元 + paymentContext.pay("alipay", 200); // 输出:使用支付宝支付 200 元 + } +} +``` + + + +### 模板方法+策略模式 + +本项目的价格试算同时用了策略模式 + 模板方法模式: + +**策略模式(Strategy)**: +`IDiscountCalculateService` 是策略接口;`ZKCalculateService`、`ZJCalculateService` ...是**可替换的折扣策略**(@Service("ZK") / @Service("ZJ") 作为选择键)。外部可以根据活动配置里的类型码选哪个实现来算价——这就是“运行时可切换算法”。 + +**模板方法模式(Template Method)**: +`AbstractDiscountCalculateService#calculate(...)` 把**共同流程**固定下来(先进行人群校验 → 计算优惠后价格),并把“**真正的计算**”这一步**延迟到子类**通过 `doCalculate(...)` 实现。 + + + ### 责任链 -应用场景:日志系统、审批流程、权限校验——任何需要将请求按阶段传递、并由某一环节决定是否继续或终止处理的地方,都非常适合职责链模式。 +应用场景:日志系统、审批流程、权限校验——任何需要将请求按阶段传递、并由某一环节决定是否继续或终止处理的地方,都非常适合责链模式。 + +![image-20250808215823342](https://pic.bitday.top/i/2025/08/08/zojzfm-0.png) + -#### 单例链 典型的责任链模式要点: @@ -700,34 +818,47 @@ public class Demo { - **动态组装**:通过 `appendNext` 可以灵活地增加、删除或重排链上的节点。 - **可扩展**:新增处理逻辑只需继承 `AbstractLogicLink` 并实现 `apply`,不用改动已有代码。 -接口定义:`ILogicChainArmory` 提供添加节点方法和获取节点 +#### 单例链 + +可以理解成“**单向、单链表式**的链条”:每个节点只知道自己的下一个节点(`next`),链头只有一个入口。 +你可以在启动或运行时**动态组装**:`head.appendNext(a).appendNext(b).appendNext(c);` + +**T / D / R 是啥?** + +- `T`:请求的**静态入参**(本次请求的主要数据)。 +- `D`:**动态上下文**(链路里各节点共享、可读写的状态容器,比如日志收集、校验中间结果)。 +- `R`:最终**返回结果**类型。 + +1)接口定义:`ILogicChainArmory` 提供**添加**节点方法和**获取**节点 ```java -//定义了责任链的组装接口: +// 定义了“链条组装”的最小能力:能拿到下一个节点、也能把下一个节点接上去 public interface ILogicChainArmory { - ILogicLink next(); //在当前节点中获取下一个节点 - - ILogicLink appendNext(ILogicLink next); //把下一个处理节点挂接上来 + // 获取当前节点的“下一个”处理者 + ILogicLink next(); + // 把新的处理者挂到当前节点后面,并返回它(方便链式 append) + ILogicLink appendNext(ILogicLink next); } ``` -`ILogicLink` 继承自 `ILogicChainArmory`,并额外声明了核心方法 `apply` +2)`ILogicLink` 继承自 `ILogicChainArmory`,并额外声明了**核心方法** `apply` ```java +// 真正的“处理节点”接口:在具备链条组装能力的基础上,还要能“处理请求” public interface ILogicLink extends ILogicChainArmory { - - R apply(T requestParameter, D dynamicContext) throws Exception; //处理请求 - + R apply(T requestParameter, D dynamicContext) throws Exception; } ``` -抽象基类:`AbstractLogicLink` +3)抽象基类:`AbstractLogicLink`,提供了**责任链节点的通用骨架**,(保存 `next`、实现 `appendNext`/`next()`、以及一个便捷的 `protected next(...)`,这样具体的节点类就不用重复这些代码,真正的业务处理逻辑仍然交由子类去实现 `apply(...)`。 ```java +// 抽象基类:大多数节点都可以继承它,避免重复写“组装链”的样板代码 public abstract class AbstractLogicLink implements ILogicLink { + // 指向“下一个处理者”的引用 private ILogicLink next; @Override @@ -738,17 +869,26 @@ public abstract class AbstractLogicLink implements ILogicLink @Override public ILogicLink appendNext(ILogicLink next) { this.next = next; - return next; + return next; // 返回 next 以便连续 append,类似 builder } + /** + * 便捷方法:当前节点决定“交给下一个处理者” + */ protected R next(T requestParameter, D dynamicContext) throws Exception { - return next.apply(requestParameter, dynamicContext); //交给下一节点处理 + // 直接把请求丢给下一个节点继续处理 + // 注意:这里假设 next 一定存在;实际项目里建议判空以免 NPE(见下文改进建议) + return next.apply(requestParameter, dynamicContext); } - } ``` -子类只需继承它,重写 `apply(...)`,在合适的条件下要么直接处理并返回,要么调用 `next(requestParameter, dynamicContext)` 继续传递。 +子类只需要继承 `AbstractLogicLink` 并实现 `apply(...)`: + +- **能处理就处理**(并可选择直接返回,终止链条)。 +- **不处理或处理后仍需后续动作**,就 `return next(requestParameter, dynamicContext)` 继续传递。 + + **使用示例:** @@ -757,6 +897,7 @@ public class AuthLink extends AbstractLogicLink { @Override public Response apply(Request req, Context ctx) throws Exception { if (!ctx.isAuthenticated()) { + // 未认证:立刻终止;也可以在这里构造一个标准错误响应返回 throw new UnauthorizedException(); } // 认证通过,继续下一个环节 @@ -778,7 +919,7 @@ public class LoggingLink extends AbstractLogicLink { ILogicLink chain = new AuthLink() .appendNext(new LoggingLink()) - .appendNext(new BusinessLogicLink()); + .appendNext(new BusinessLogicLink()); // 作为终结节点 //客户端使用 Request req = new Request(...); @@ -970,78 +1111,43 @@ TrialBalanceEntity result = -### 策略模式 +## 收获 -**核心思想**: -把可互换的算法/行为抽成独立策略类,运行时由“上下文”对象选择合适的策略;对调用方来说,只关心统一接口,而非具体实现。 +### 实体对象 -```text -┌───────────────┐ -│ Client │ -└─────▲─────────┘ - │ has-a -┌─────┴─────────┐ implements -│ Context │────────────┐ ┌──────────────┐ -│ (使用者) │ strategy └─▶│ Strategy A │ -└───────────────┘ ├──────────────┤ - │ Strategy B │ - └──────────────┘ +实体是指具有唯一标识的业务对象。 -``` +在 DDD 分层里,**Domain Entity ≠ 数据库 PO**。 +在 `edu.whut.domain.*.model.entity` 包下放的是**纯粹的业务对象**,它们只表达业务语义(团队 ID、活动时间、优惠金额……),对「数据持久化细节」保持**无感知**。因此它们看起来“字段不全”是正常的: -#### 集合自动注入 - -常见于策略/工厂/插件场景。 +- 它们不会带 `@TableName` / `@TableId` 等 MyBatis-Plus 注解; +- 也不会出现数据库的技术字段(`id`、`create_time`、`update_time`、`status` 等); +- 只保留聚合根真正**需要的**业务属性与行为。 ```java -@Autowired -private Map discountCalculateServiceMap; -``` +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PayActivityEntity { -**字段类型**:`Map` + /** 拼单组队ID */ + private String teamId; + /** 活动ID */ + private Long activityId; + /** 活动名称 */ + private String activityName; + /** 拼团开始时间 */ + private Date startTime; + /** 拼团结束时间 */ + private Date endTime; + /** 目标数量 */ + private Integer targetCount; -- key—— **Bean 的名字** - - 默认是类名首字母小写 (`mjCalculateService`) - - 或者你在实现类上显式写的 `@Service("MJ")` -- **value** —— 那个实现类对应的**实例** -- **Spring 机制**: - 1. 启动时扫描所有实现 `IDiscountCalculateService` 的 Bean。 - 2. 把它们按 “BeanName → Bean 实例” 的映射注入到这张 `Map` 里。 - 3. 你一次性就拿到了“策略字典”。 - -**示例:** - -```java -@Service("MJ") // ★ 关键:Bean 名即策略键 -public class MJCalculateService extends IDiscountCalculateService { - - @Override - protected BigDecimal Calculate(String userId, BigDecimal originalPrice, - GroupBuyActivityDiscountVO.GroupBuyDiscount groupBuyDiscount) { - //忽略实现细节 -} - -@Component -@RequiredArgsConstructor // 构造器注入更推荐 -public class DiscountContext { - - private final Map discountServiceMap; - - public BigDecimal calc(String strategyKey, - String userId, - BigDecimal originalPrice, - GroupBuyActivityDiscountVO.GroupBuyDiscount plan) { - //strategyKey可以是"MJ" .. - IDiscountCalculateService strategy = discountServiceMap.get(strategyKey); - if (strategy == null) { - throw new IllegalArgumentException("无匹配折扣类型: " + strategyKey); - } - return strategy.calculate(userId, originalPrice, plan); - } } ``` - +这个也是实体对象,因为多个字段的组合: teamId 和 activityId 能唯一标识这个实体。 @@ -1122,30 +1228,16 @@ public class SimpleAsyncDemo { ### 动态配置(热更新) -原理:借助 Redis 的发布/订阅(Pub/Sub)能力,在程序跑起来以后,动态地往某个频道推送一条消息,然后所有订阅了该频道的 Bean 都会收到通知,进而反射更新它们身上的对应字段。 +**原理:**利用 Redis 的发布/订阅(Pub/Sub)机制,在程序运行时动态推送配置变更通知,订阅者接收到消息后更新相应的 Bean 字段。通过 **反射(Reflection API)** 可以**动态修改运行中的对象实例的字段值**。 + +![image-20250808160333248](https://pic.bitday.top/i/2025/08/08/qigigf-0.png) -```text -启动时 ────────────────────────────────────▶ BeanPostProcessor - │ 扫描 @DCCValue 写入默认 / 读取 Redis - │ 注入字段值 缓存 key→Bean -───────────────────────────────────────────────────────────────── -运行时 - 管理后台调用 ───▶ publish("myKey,newVal") ───▶ Redis Pub/Sub - │ │ - │ ▼ - │ RTopic listener 收到消息 - │ └─ ▸ 写回 Redis - │ └─ ▸ 从 Map 找到 Bean - │ └─ ▸ 反射注入新值到字段 - ▼ -Bean 字段热更新完成 -``` #### 实现步骤 **注解标记** -用 `@DCCValue("key:default")` 标注需要动态注入的字段,指定对应的 Redis Key(带前缀)及默认值。 +用 `@DCCValue("key:default")` 标注需要动态注入的字段,指定 Redis Key 和默认值。 ```java // 标记要动态注入的字段 @@ -1159,112 +1251,110 @@ public @interface DCCValue { // 业务使用示例 @Service public class MyFeature { - @DCCValue("myFlag:0") + @DCCValue("myFlag:0") //标注字段,默认值为0 private String myFlag; public boolean enabled() { return "1".equals(myFlag); } } ``` **启动时注入** -实现一个 `BeanPostProcessor`,在每个 Spring Bean 初始化后: +实现 `BeanPostProcessor`,覆写`postProcessAfterInitialization`方法,在每个 Spring Bean 初始化后自动执行: -- 扫描带 `@DCCValue` 的字段; -- 拼出完整 Redis Key(如 `dcc_prefix_key`),若不存在则写入默认值,否则读最新值; -- **反射把值注入到该 Bean 的私有字段**; -- 将 `(redisKey → Bean 实例)` 记录到内存映射,用于后续热更新。 +- 扫描标注了 `@DCCValue` 的字段; +- 拼接完整 Redis Key,若 Redis 中没有配置,则写入默认值; +- 通过反射将配置值注入到 Bean 的字段; +- 将配置与 Bean 映射关系存入内存,以便后续热更新。 ```java @Override public Object postProcessAfterInitialization(Object bean, String name) { - // 确定真实的目标类:处理代理 Bean 或普通 Bean - Class cls = AopUtils.isAopProxy(bean) - ? AopUtils.getTargetClass(bean) - : bean.getClass(); + Class cls = AopUtils.isAopProxy(bean) ? AopUtils.getTargetClass(bean) : bean.getClass(); - // 遍历所有字段,寻找标注了 @DCCValue 的配置字段 for (Field f : cls.getDeclaredFields()) { - DCCValue dv = f.getAnnotation(DCCValue.class); - if (dv == null) { - continue; // 如果该字段未被 @DCCValue 注解标注,则跳过 + DCCValue dccValue = f.getAnnotation(DCCValue.class); + if (dccValue != null) { + String[] parts = dccValue.value().split(":"); + String key = PREFIX + parts[0]; // Redis 中存储的 Key + String defaultValue = parts[1]; // 默认值 + + RBucket bucket = redis.getBucket(key); + String value = bucket.isExists() ? bucket.get() : defaultValue; + bucket.trySet(defaultValue); // 若 Redis 中无配置,则写入默认值 + + injectField(bean, f, value); // 通过反射注入值 + beans.put(key, bean); // 缓存配置与 Bean 映射关系 } - - // 注解值格式为 "key:default",拆分获取配置项的 key 和默认值 - String[] parts = dv.value().split(":"); - String key = PREFIX + parts[0]; // Redis 中存储该配置的完整 Key - String defaultValue = parts[1]; // 默认值 - - // 从 Redis 获取配置,如果不存在则使用默认值,并同步写入 Redis - RBucket bucket = redis.getBucket(key); - String val = bucket.isExists() ? bucket.get() : defaultValue; - bucket.trySet(defaultValue); // 如果 Redis 中没有该 Key,则写入默认值 - - // 反射方式将值注入到 Bean 的字段上(即动态替换该字段的值) - injectField(bean, f, val); - - // 将该 Bean 注册到映射表,以便后续热更新时找到实例并更新字段 - beans.put(key, bean); } - - return bean; // 返回处理后的 Bean + return bean; // 返回初始化后的 Bean } - ``` **运行时热更新** -- 在同一个组件里,订阅一个 Redis Topic(频道),比如 `"dcc_update"`; +- 订阅一个 Redis Topic(频道),比如 `"dcc_update"`; -- 外部调用发布接口 `PUBLISH dcc_update "key,newValue"`; +- 外部通过发布接口 `PUBLISH dcc_update "key,newValue"` 发送更新消息; ```java - //更新配置 + private final RTopic dccTopic; @GetMapping("/dcc/update") public void update(@RequestParam String key, @RequestParam String value) { - dccTopic().publish(key + "," + value); + // 发布配置更新消息到 Redis 主题,格式为 "configKey,newValue" + String message = key + "," + value; + dccTopic.publish(message); // 通过 dccTopic 发布更新消息 + log.info("配置更新发布成功 - key: {}, value: {}", key, value); } ``` - 订阅者收到后: - 1. 同步把新值写回 Redis; - 2. 从映射里取出对应 Bean,反射更新它的字段。 + 1. 更新 Redis 中的配置; + 2. 从映射里取出对应 Bean,使用反射更新字段。 ```java -// 发布/订阅频道,用于接收 DCC 配置的热更新消息 -@Bean -public RTopic dccTopic() { - // 1. 从 RedissonClient 中获取名为 "dcc_update" 的主题(Topic),后续会订阅这个频道 - RTopic t = redis.getTopic("dcc_update"); +// 发布/订阅配置热更新 +@Bean("dccTopic") +public RTopic dccTopic(RedissonClient redis) { + RTopic dccTopic = redis.getTopic("dcc_update"); - // 2. 为该主题添加监听器,消息格式为 String - t.addListener(String.class, (channel, msg) -> { - // 3. msg 约定格式:"configKey,newValue",先按逗号分割出 key 和 value - String[] a = msg.split(","); - String key = PREFIX + a[0]; // 拼出完整的 Redis Key - String val = a[1]; // 新的配置值 + dccTopic.addListener(String.class, (channel, msg) -> { + String[] parts = msg.split(","); // msg 约定格式:"configKey,newValue" + String key = PREFIX + parts[0]; // 拼接 Redis Key + String newValue = parts[1]; // 新的配置值 - // 4. 检查 Redis 中是否已存在该 Key(只对已注册的配置生效) RBucket bucket = redis.getBucket(key); if (!bucket.isExists()) { - return; // 如果不是我们关心的配置,跳过 + return; // 如果不是我们关心的配置,跳过 } - // 5. 把新值同步写回 Redis,保证持久化 - bucket.set(val); + bucket.set(newValue); // 更新 Redis 中的配置 - // 6. 从内存缓存中取出当初注入该 key 的 Bean 实例 - Object bean = beans.get(key); + Object bean = beans.get(key); // 从内存中取出 Bean 实例 if (bean != null) { - // 7. 通过反射把新的配置值重新注入到 Bean 的字段上,完成热更新 - injectField(bean, a[0], val); + injectField(bean, parts[0], newValue); // 通过反射更新 Bean 字段 } }); - // 8. 返回这个 RTopic Bean,让 Spring 容器管理 - return t; + return dccTopic; // 返回 Redis Topic 实例 } + ``` +在 Redis 的发布/订阅模型中,`RTopic dccTopic = redis.getTopic("dcc_update");` 这行代码指定了 `dccTopic` 订阅的主题(也可以理解为一个消息通道)。不同的类可以通过依赖注入来使用这个 `RTopic` 实例。一些类可以调用 `dccTopic.publish(message)` 向该通道发送消息;而另一些类则可以通过 `dccTopic.addListener()` 来订阅该主题,从而接收消息并进行相应的处理。 + + + +#### 热更新数据流转过程 + +**1.广播消息(PUBLISH)**:配置变更会通过 `PUBLISH` 命令广播到 Redis 中的某个主题。 + +**2.Redis Sub(订阅)**:订阅该主题的客户端收到消息后,进行处理。 + +**3.更新 Redis 和 Bean 字段**: + +- 更新 Redis 中的配置(保持一致性)。 +- 更新 Bean 实例的对应字段(通过反射,确保配置的实时性)。 + ### OkHttpClient @@ -1280,77 +1370,89 @@ public RTopic dccTopic() { **让Spring 管理 Http客户端** -- 写配置类 +写配置类 - ```java - @Configuration - public class OKHttpClientConfig { - - @Bean - public OkHttpClient httpClient() { - return new OkHttpClient(); - } - } - ``` +```java +@Configuration +public class OKHttpClientConfig { -- 在需要使用的地方注入 + @Bean + public OkHttpClient httpClient() { + return new OkHttpClient(); + } +} +``` - ```java - @Slf4j - @Service - @RequiredArgsConstructor - public class HttpService { - - private final OkHttpClient okHttpClient; - - /** - * 发送 JSON POST 请求并返回响应内容 - * - * @param apiUrl 接口地址 - * @param jsonPayload 请求体 JSON 字符串 - */ - public String postJson(String apiUrl, String jsonPayload) throws IOException { - //1.构建参数 - MediaType mediaType = MediaType.get("application/json; charset=utf-8"); - RequestBody body = RequestBody.create(jsonPayload, mediaType); - Request request = new Request.Builder() - .url(apiUrl) - .post(body) - .addHeader("Content-Type", "application/json") - .build(); - //2.调用接口 - try (Response response = okHttpClient.newCall(request).execute()) { - if (!response.isSuccessful()) { - log.error("HTTP 请求失败,URL:{},状态码:{}", apiUrl, response.code()); - throw new IOException("Unexpected HTTP code " + response.code()); - } - ResponseBody responseBody = response.body(); - return responseBody != null ? responseBody.string() : ""; - } catch (IOException e) { - log.error("调用 HTTP 接口异常:{}", apiUrl, e); - throw e; - } - } - } - ``` +在需要使用的地方注入 -- 优点: +```java +@Slf4j +@Service +@RequiredArgsConstructor +public class HttpService { - 单例复用,性能更优 + private final OkHttpClient okHttpClient; - - Spring 默认将 Bean 作为单例管理,整个应用只创建一次 `OkHttpClient`。 - - 内部的连接池、线程池、缓存等资源可以被复用,**避免频繁创建、销毁**带来的开销。 + /** + * 发送 JSON POST 请求并返回响应内容 + * + * @param apiUrl 接口地址 + * @param jsonPayload 请求体 JSON 字符串 + */ + public String postJson(String apiUrl, String jsonPayload) throws IOException { + //1.构建参数 + MediaType mediaType = MediaType.get("application/json; charset=utf-8"); + RequestBody body = RequestBody.create(jsonPayload, mediaType); + Request request = new Request.Builder() + .url(apiUrl) + .post(body) + .addHeader("Content-Type", "application/json") + .build(); + //2.调用接口 + try (Response response = okHttpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + log.error("HTTP 请求失败,URL:{},状态码:{}", apiUrl, response.code()); + throw new IOException("Unexpected HTTP code " + response.code()); + } + ResponseBody responseBody = response.body(); + return responseBody != null ? responseBody.string() : ""; + } catch (IOException e) { + log.error("调用 HTTP 接口异常:{}", apiUrl, e); + throw e; + } + } +} +``` - 统一配置,易于维护 +优点: + +单例复用,性能更优 + +- Spring 默认将 Bean 作为单例管理,整个应用只创建一次 `OkHttpClient`。 +- 内部的连接池、线程池、缓存等资源可以被复用,**避免频繁创建、销毁**带来的开销。 + +统一配置,易于维护 + +- 超时、拦截器、连接池、SSL、日志等配置集中在一个地方,改动一次全局生效。 +- 避免在代码各处手动 `new OkHttpClient()`、重复配置。 - - 超时、拦截器、连接池、SSL、日志等配置集中在一个地方,改动一次全局生效。 - - 避免在代码各处手动 `new OkHttpClient()`、重复配置。 - ### Retrofit -微信登录时,需要调用微信提供的接口做验证。 +**Retrofit** 适用于: + +- **第三方服务集成**:如调用 Web 服务,RESTful API 等。 +- **API 请求封装**:在 Java 或 Android 中,简化 HTTP 请求与响应的处理。 +- **灵活的调用方式**:支持同步和异步的请求,适用于处理外部接口的调用。 + +**RPC** 适用于: + +- **微服务架构**:内部服务之间需要高效、低延迟的通信。 +- **跨语言服务**:支持多种编程语言间的通信,特别是采用类似 gRPC 这样的协议。 +- **高吞吐量、低延迟**:RPC 常常用于对性能要求较高的系统,尤其是微服务通信。 + + #### 快速入门 @@ -1456,18 +1558,10 @@ Retrofit 在运行时会生成这个接口的实现类,帮你完成: -| 核心点 | Apache HttpClient | Retrofit | -| --------------- | ----------------------------------------- | ------------------------------------------------------------ | -| 编程模型 | 细粒度调用,手动构造 `HttpGet`/`HttpPost` | 注解驱动接口方法,声明式调用 | -| 请求定义 | 手动拼接 URL、参数 | 用 `@GET`/`@POST`、`@Path`、`@Query`、`@Body` 注解 | -| 序列化/反序列化 | 手动调用 `ObjectMapper`/`Gson` | 自动通过 `ConverterFactory`(Jackson/Gson 等) | -| 同步/异步 | 以同步为主,异步需自行管理线程和回调 | 同一个 `Call` 即可 `execute()`(同步)或 `enqueue()`(异步) | -| 扩展性与拦截器 | 可配置拦截器,但需手动集成 | 底层基于 OkHttp,天然支持拦截器、连接池、缓存、重试和取消 | - - - ### 公众号扫码登录流程 +微信登录时,需要调用微信提供的接口做验证,使用**Retrofit** + 场景:用微信的能力来替你的网站做“扫码登录”或“社交登录”,代替自己写一整套帐号/密码体系。后台只需要基于 `openid` 做一次性关联(比如把某个微信号和你系统的用户记录挂钩),后续再次扫码就当作同一用户; ![image-20250711192110034](https://pic.bitday.top/i/2025/07/11/vrgj6u-0.png) @@ -1507,6 +1601,8 @@ Retrofit 在运行时会生成这个接口的实现类,帮你完成: + + ### 浏览器指纹获取登录ticket 在扫码登录流程的基础上改进!!! @@ -1577,39 +1673,6 @@ ticketOpenidCache.put(ticket, openid); // 保存 ticket→openid -### 无痕登录 - -“无痕登录”(又称“免扫码登录”或“静默登录”)的核心思想,是在用户首次通过二维码/授权完成登录后,给这台设备发放一份**长期信任凭证**,以后再访问就能悄无声息地登录,不再需要人为地再扫码或输入密码。 - -#### 典型流程 - -**1.初次登录(扫码授权)** - -即前面**"浏览器指纹获取登录ticket"**的流程 - -**2.后续“无痕”自动登录** - -1)前端再次打开页面,重新生成指纹 - -2)前端调用“免扫码”接口,仅传递指纹 - -3)后端校验 fingerprint → openid - -```java -String openid = sceneLoginCache.getIfPresent(sceneStr); -if (openid != null) { - // 直接返回登录态(Session / JWT) - return SUCCESS(openid); -} else { - // 指纹过期或未绑定,返回未登录,前端再走扫码流程 - return NO_LOGIN; -} -``` - -4)**成功后**,前端拿到 openid/JWT,直接进入应用,无需用户任何操作。 - - - ### 独占锁和无锁化场景(防超卖) #### 独占锁 @@ -1668,47 +1731,53 @@ if (openid != null) { 票务分配、座位预占,都讲究“先到先得”+“补偿回退”,不能用一把大锁。 ```java -@Override -public boolean tryOccupy(String counterKey, - String recoveryKey, - int target, - int ttlMinutes) { +public boolean occupyTeamStock(String teamOccupiedStockKey, String recoveryTeamStockKey, Integer target, Integer validTime) { + // 获取失败恢复量(系统异常时记录的可回收库存) + Long recoveryCount = redisService.getAtomicLong(recoveryTeamStockKey); + recoveryCount = (recoveryCount == null) ? 0 : recoveryCount; - // 1) 读取“补偿”次数(退款/回滚补偿) - Long recovery = redisService.getAtomicLong(recoveryKey); - int recovered = (recovery == null ? 0 : recovery.intValue()); + // 自增占用量,+1 表示团长开团已占一单 + long occupy = redisService.incr(teamOccupiedStockKey) + 1; - // 2) 原子自增,拿到当前序号 - long seq = redisService.incr(counterKey); - long occupySeq = seq; - - // 3) 超出“目标 + 补偿池” → 回滚主计数器,失败 - if (occupySeq > target + recovered) { - redisService.setAtomicLong(counterKey, target); + // 超出可用库存(目标值 + 恢复量)则失败 + if (occupy > target + recoveryCount) { return false; } - // 4) 如果用到了补偿名额(序号已经 > target),就从补偿池里减掉一个 - //if (occupySeq > target) { - // redisService.decr(recoveryKey); - //} + // 兜底:为每个序号加分布式锁,防止极端情况下序号重复 + // 过期时间比 validTime 多 60 分钟,便于排查问题 + String lockKey = teamOccupiedStockKey + Constants.UNDERLINE + occupy; + Boolean lock = redisService.setNx(lockKey, validTime + 60, TimeUnit.MINUTES); - // 5) 兜底锁:针对每个序号做一次 SETNX,防止极端重复 - String lockKey = counterKey + ":lock:" + occupySeq; - boolean locked = redisService.setNx(lockKey, ttlMinutes, TimeUnit.MINUTES); - if (!locked) { - return false; + if (!lock) { + log.info("组队库存加锁失败 {}", lockKey); } - // 6) 成功占位 - return true; + return lock; } ``` +**注意,**这里的锁单量teamOccupiedStockKey是**Redis中的**,非mysql中的!!!因此锁单量不会减少!当用户退款后,redis中恢复量recoveryCount会+1。 + +即这两个量都是递增的,不要与mysql中的lock_count混淆了。 + 本项目有两层防护:第一层是下单前的人数/库存校验,比较基础,由于前端可能更新不及时,显示还差X人拼团,但用户点进去时已达人数的情况。第二层是真正的并发保证,即**Redis 原子操作** + **后置校验/补偿**。 +##### 生活例子理解 + +假设你有一个限量商品,每个商品有一个唯一的编号,假设这些商品编号为 1、2、3、4、5(总共 5 个)。这些商品被分配给用户,每个用户会抢一个编号。每个用户成功抢到一个商品后,系统会在库存中占用一个编号。 + +**抢购过程:** + +- 有 5 个商品编号(1-5),这些编号是**库存量**。 +- 每个用户请求一个商品编号,系统会给用户分配一个编号(这个过程就像是自增占用量的过程)。 +- 如果用户请求的编号超过了现有库存的最大编号(5),则说明没有商品可以分配给该用户,用户抢购失败。 +- 如果有多个用户抢同一个编号(例如都想抢到编号 1 的商品),系统通过“分布式锁”来保证只有一个用户能成功抢到编号 1,其他用户则失败。 + + + ### `Supplier` `Supplier` 是 Java 8 提供的一个函数式接口 @@ -1767,37 +1836,62 @@ List list = getFromCacheOrDb( -### 动态限流+黑名单 +### 分布式限流(AOP + Redisson 实现)+黑名单 ce1092e98bdb7d396589a46376b872a4 +#### 核心思路 + +**动态开关管理** + +- 使用 `@DCCValue("rateLimiterSwitch:open")` 从配置中心动态注入全局开关,支持热更新。 +- 当开关为 `"close"` 时,直接放行所有请求,切面不再执行限流逻辑。 + +**AOP 切面拦截** + +- 通过自定义注解 `@RateLimiterAccessInterceptor` 标记需要限流的方法。 +- 注解参数 `key` 用于指定限流维度(如 `userId` 表示按用户限流,`all` 表示全局限流)。 +- 切面在运行时解析这个字段的值,动态生成 Redis 限流器 Key,例如: + +```java +//添加拦截注解 +@RateLimiterAccessInterceptor(key = "userId", permitsPerSecond = 5, fallbackMethod = "fallback") +public void order(String userId) {...} + +请求1: userId=U12345 → Redis Key: rl:limiter:U12345 +请求2: userId=U67890 → Redis Key: rl:limiter:U67890 +``` + + + +**限流与黑名单** + +- 使用 `RRateLimiter` 实现分布式令牌桶,每秒放入 `permitsPerSecond` 个令牌。 +- 取不到令牌时: + - 如果配置了 `blacklistCount`,用 `RAtomicLong` 记录该 Key 的拒绝次数; + - 拒绝次数超限后,将 Key 加入黑名单 24 小时。 +- 命中黑名单或限流时,调用注解里的 `fallbackMethod` 执行降级逻辑。 + **令牌桶算法(Token Bucket)** -- 按固定速率往桶里放“令牌”(tokens),比如每秒放 N 个; -- 每次请求来临时“取一个令牌”才能通过,取不到就拒绝或降级; -- 可以做到“流量平滑释放”、“突发流量吸纳”(桶里最多能积攒 M 个令牌)。 - -**核心限流思路** - -- **注解驱动拦截**:对标记了 `@RateLimiterAccessInterceptor` 的方法统一进行限流。 -- **分布式限流**:基于 Redisson 的 `RRateLimiter`,可在多实例环境下共享令牌桶。 -- **黑名单机制**:对超限用户计数,达到阈值后加入黑名单(24 h 后自动解禁)。 -- **动态开关**:通过 DCC 配置中心开关(`rateLimiterSwitch`)可随时启用或关闭限流。 -- **降级回调**:限流或黑名单命中时,通过注解指定的方法反射调用,返回自定义响应。 +- **工作原理**:按固定速率往桶里放“令牌”(tokens),例如每秒放 N 个。每次请求到达时,必须先从桶中“取一个令牌”,才能通过;如果取不到,则拒绝或降级。 +- **特点**:支持流量平滑释放和突发流量吸纳,桶最多能存储 M 个令牌。 ```text -请求到达 +方法调用 ↓ -检查限流开关(DCC) +AOP 切面拦截(匹配 @RateLimiterAccessInterceptor) ↓ -解析限流维度(key,如 userId) +检查全局限流开关(@DCCValue 注入) ↓ -黑名单校验(RAtomicLong 计数,24h 过期) +解析注解里的 key → 获取对应参数值(如 userId) + ↓ +黑名单检查(RAtomicLong) ↓ 分布式令牌桶限流(RRateLimiter.tryAcquire) ↓ -├─ 通过 → 执行目标方法 -└─ 拒绝 → 调用 fallback 方法,记录黑名单次数 +├─ 成功 → 执行目标方法 +└─ 失败 → 累加拒绝计数 & 调用 fallbackMethod ``` @@ -1808,10 +1902,8 @@ List list = getFromCacheOrDb( | 性能开销 | **极低**:全程内存操作,纳秒级延迟 | **中等**:每次获取令牌需网络往返,存在 RTT 延迟 | | 限流范围 | **单实例**:仅对当前 JVM 有效,多实例互不影响 | **全局**:多实例共享同一套令牌桶,合计速率可控 | | 状态持久化 & 容错 | **无**:服务重启后状态丢失;实例宕机只影响自身 | **有**:Redis 存储限流器与黑名单,可持久化;需保证 Redis 可用性 | -| 监控 & 可观测 | **弱**:需额外上报或埋点才能集中监控 | **强**:可直接查看 Redis Key、TTL、计数等,易做报警与可视化 | -| 运维依赖 | **无**:不依赖外部组件 | **有**:需维护高可用的 Redis 集群,增加运维成本 | -目前本项目使用的是分布式限流,用Redisson +目前本项目采用 **分布式限流**,使用 **Redisson** 实现跨实例令牌桶,确保全局限流控制。 @@ -1938,38 +2030,53 @@ output { ### 防止重复下单 -**外部交易单号设计** +#### 前端限制 + +- 点击下单按钮后,将按钮设置为禁用状态 + +#### 后端限制 + +即使前端做了按钮禁用,还是可能存在用户通过其他方式发起多个请求。 + -- **统一跟踪**:对接小商城时,将外部交易单号(`out_trade_no`)与小商城下单时生成的 `order_id` 保持一致,方便全链路追踪。 -- **内部独立**:拼团系统内部仍保留自己的 `order_id`,互不冲突。 在高并发支付场景中,确保同一用户对同一商品/活动只生成一条待支付订单,常用以下两种思路: #### 业务维度复合唯一索引 + 冲突捕获重试 -1. **查询未支付订单** - - 在创建订单时,先根据业务维度(如 `userId + goodId + activityId`)查询“已下单但未支付”的订单; - - 若存在,则直接返回该订单,避免二次创建。 -2. **复合唯一索引约束** - - 在订单表中对业务维度字段(`userId`、`goodId`、`activityId` 等)添加**复合唯一索引**; - - 高并发下若出现并行插入,后续请求因违反唯一约束抛出异常; - - 捕获异常后,再次查询并返回已创建的订单,实现幂等。 -3. **分布式锁保障(可选)** - - 针对同一用户加分布式锁(例如 `lock:userId:{userId}`),确保只有**首个请求能获取锁**并创建订单; - - 后续请求等待锁释放或直接返回“订单处理中”,随后再次查询订单状态。 +- 利用业务维度字段(`userId` + `goodId` + `activityId`)创建复合唯一索引,避免重复下单。 + +- 通过查询数据库检查是否已有未支付订单,若有则直接返回该订单。 + +- 若并发创建订单导致唯一约束冲突,捕获异常后重新查询返回已创建订单。 + +- 可选:使用分布式锁来控制高并发环境中的锁操作,确保只有首个请求能够创建订单。 #### 幂等 Key 模式 +**外部交易单号设计** + +- **统一跟踪**:对接小商城时,将外部交易单号(`out_trade_no`)与小商城下单时生成的 `order_id` 保持一致,方便全链路追踪。 +- **内部独立**:拼团系统内部仍保留自己的 `order_id`,互不冲突。 + 1. **生成幂等 Key** + - 前端进入支付流程时调用接口(`GET /api/idempotency-key`),后端生成全局唯一 ID(UUID 或雪花 ID)返回给前端; - 或者外部系统(如小商城)传来唯一的外部交易单号(`out_trade_no`),**天生作为幂等Key。** - 前端将该 Key 存入内存、LocalStorage 或隐藏表单字段,直至支付完成或过期。 2. **请求携带幂等 Key** + - 用户点击“下单”时,调用 `/create_pay_order` 接口,需在请求体中附带 `idempotencyKey`; - 服务端根据该 Key 判断:若数据库中已有相同 `idempotency_key`,直接返回该订单,否则创建新订单。 3. **数据库持久化 & 唯一约束** - 在订单表中新增 `idempotency_key` 列,并对其增加唯一索引; - 双重保障:前端重复发送同一 Key,也仅能插入一条记录,彻底避免重复下单。 + + + +**总结:本质上还是通过数据库唯一索引以及分布式锁才能彻底避免重复下单。** + + @@ -2008,3 +2115,140 @@ output { ``` **** + +3.部署nacos(详见微服务笔记) + +4.配置注册(消费者、生产者都要配) + +```yml +dubbo: + application: + name: group-buy-market-service # 换成各自服务名 + registry: + address: nacos://localhost:8848 # 远程环境写内网地址 + # username/password 如果 Nacos 开了鉴权 + protocol: + name: dubbo + port: 20880 # 生产者开放端口;消费者可不写 + consumer: + timeout: 3000 # 毫秒 + check: false # 忽略启动时服务是否可用 +``` + +5.开启 Dubbo 注解扫描 + +在消费者、生产者的主启动类上加,设置正确的包名,让 `@DubboService` 和 `@DubboReference` 被 Spring+Dubbo 识别和处理 + +```java +@SpringBootApplication +@EnableDubbo(scanBasePackages = "edu.whut") +public class Application { … } +``` + +6.在Dubbo RPC调用中,DTO对象需要在网络中进行传输,因此它们必须实现 `java.io.Serializable` 接口: + +```java +/** + * 用户信息请求对象 + */ +@Data +public class UserRequestDTO implements Serializable { // 实现 Serializable + private static final long serialVersionUID = 1L; // 添加 serialVersionUID,用于版本控制 + + // 用户ID + private String userId; + // 用户名 + private String userName; + // 邮箱 + private String email; +} +``` + +7.定义服务接口: + +服务接口定义了服务提供者能够提供的功能以及服务消费者能够调用的方法。这个接口必须是**公共的**,并且通常放置在一个独立的 `api`模块中。供服务提供者和消费者共同依赖。 + +```java +/** + * 用户服务接口 + */ +public interface IUserService { + /** + * 根据用户ID获取用户信息 + * @param requestDTO 用户请求对象 + * @return 用户响应对象 + */ + UserResponseDTO getUserInfo(UserRequestDTO requestDTO); + + /** + * 创建新用户 + * @param requestDTO 用户请求对象 + * @return 操作结果 + */ + String createUser(UserRequestDTO requestDTO); +} +``` + +8.服务提供者 (Provider) 实现并暴露服务 + +在服务提供者应用中,实现上述定义的服务接口,并使用 `@DubboService` 注解将其暴露为Dubbo服务。可以放在trigger/rec包下。 + +```java +/** + * 用户服务实现类 + */ +@DubboService(version = "1.0.0", group = "user-service") // 关键注解:暴露Dubbo服务 +@Service // 也可以同时是Spring的Service +public class UserServiceImpl implements IUserService { + + @Override + public UserResponseDTO getUserInfo(UserRequestDTO requestDTO) { + System.out.println("收到获取用户信息的请求: " + requestDTO.getUserId()); + // 模拟业务逻辑 + UserResponseDTO response = new UserResponseDTO(); + response.setUserId(requestDTO.getUserId()); + response.setUserName("TestUser_" + requestDTO.getUserId()); + response.setEmail("test_" + requestDTO.getUserId() + "@example.com"); + return response; + } + + @Override + public String createUser(UserRequestDTO requestDTO) { + System.out.println("收到创建用户的请求: " + requestDTO.getUserName()); + // 模拟业务逻辑 + return "User " + requestDTO.getUserName() + " created successfully."; + } +} +``` + +9.服务消费者 (Consumer) 引用远程服务 + +在服务消费者应用中,通过 `@DubboReference` 注解引用远程Dubbo服务。Dubbo 会自动通过注册中心查找并注入对应的服务代理。 + +```java +/** + * 用户API控制器 + */ +@RestController +public class UserController { + + @DubboReference(version = "1.0.0", group = "user-service") // 关键注解:引用Dubbo服务 + private IUserService userService; + + @GetMapping("/user/info") + public UserResponseDTO getUserInfo(@RequestParam String userId) { + UserRequestDTO request = new UserRequestDTO(); + request.setUserId(userId); + return userService.getUserInfo(request); + } + + @GetMapping("/user/create") + public String createUser(@RequestParam String userName, @RequestParam String email) { + UserRequestDTO request = new UserRequestDTO(); + request.setUserName(userName); + request.setEmail(email); + return userService.createUser(request); + } +} +``` +