refactor: re-organise doc structure
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Cooking Seasoning Guide
|
||||
tags:
|
||||
- cooking
|
||||
- cheatsheet
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
## When to Add Seasonings
|
||||
|
||||
| Seasoning Type | Timing | Suitable Dishes | Effect |
|
||||
|----------------------------------------|----------------------------------------|------------------------------------------------------|-------------------------------------------------------------------------------|
|
||||
| **Salt** | Last | Leafy greens | Prevents leaves from wilting and releasing water, keeps them crisp and tender |
|
||||
| | Mid-way | Shredded potatoes, green beans, garlic shoots | Better flavour absorption |
|
||||
| **MSG, Light Soy Sauce, Oyster Sauce** | Towards the end | Dishes needing umami and aroma enhancement | Adds umami and aroma; high heat destroys freshness and fragrance |
|
||||
| **Cooking Wine** | Together with ingredients | Ingredients needing deodorisation | Better deodorisation when blanching |
|
||||
| | At highest wok heat | Stir-fried ingredients needing deodorisation | High heat accelerates alcohol evaporation, effective aroma searing |
|
||||
| **Vinegar** | Early | Hot and sour shredded potatoes, hot and sour cabbage | Makes shredded potatoes crisper and more flavourful |
|
||||
| | Drizzle around wok edge before serving | Stir-fried chilli pork and similar high-heat dishes | High-heat searing enhances aroma |
|
||||
| **Dark Soy Sauce** | After half-cooked | Dishes needing colour | Achieves optimal colouring effect |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Seasoning | Timing | Effect | Example |
|
||||
|-----------------------------------------------|------------------------------|-----------------------------------------------------------------------------|---------------------------------------------------------------------------|
|
||||
| **Salt** | 10–15 seconds before serving | Prevents premature water loss and wilting of leafy greens; keeps them crisp | Sprinkle salt and toss before turning off the heat for lettuce or spinach |
|
||||
| **Minced Garlic / Ginger** | First, after oil is hot | Releases fragrance and enhances flavour | Sauté minced garlic in hot oil, then add greens |
|
||||
| **Light Soy Sauce / Oyster Sauce** (optional) | After salt | Adds umami; use sparingly to avoid overpowering the vegetables | Add 1–2 drops of light soy sauce after salting Shanghai greens |
|
||||
| **Cooking Oil** | Hot wok, hot oil | Quick stir-fry at high heat locks in moisture | Use slightly more oil and high heat for fast stir-frying |
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Docker Deployment Standards
|
||||
tags:
|
||||
- docker
|
||||
- deployment
|
||||
- standards
|
||||
- best-practice
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
- **Dockerfiles**: Provide a `Dockerfile` for the application to enable containerised deployment.
|
||||
- **Lightweight Images**: Strive for lightweight Docker images by using appropriate base images and multi-stage builds.
|
||||
- **Configuration**: Ensure environment-specific configuration (e.g., database connection strings, external service
|
||||
URLs) is managed through environment variables injected into Docker containers.
|
||||
- **Logging**: Configure containerised logging to output to `stdout` and `stderr` so that log aggregation systems can
|
||||
collect logs easily.
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: ECMAScript 2025 Syntax Sugar Guide
|
||||
tags:
|
||||
- javascript
|
||||
- ecmascript
|
||||
- pattern-matching
|
||||
- frontend
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
## Pattern Matching
|
||||
|
||||
### Traditional Approach
|
||||
|
||||
```javascript
|
||||
function processResponse(response) {
|
||||
if (response.status === 200 && response.data) {
|
||||
return { success: true, data: response.data };
|
||||
} else if (response.status === 404) {
|
||||
return { success: false, error: 'Not found' };
|
||||
} else if (response.status >= 500) {
|
||||
return { success: false, error: 'Server error' };
|
||||
} else {
|
||||
return { success: false, error: 'Unknown error' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern Matching Approach
|
||||
|
||||
```javascript
|
||||
function processResponse(response) {
|
||||
return match (response) {
|
||||
when ({ status: 200, data }) -> ({ success: true, data })
|
||||
when ({ status: 404 }) -> ({ success: false, error: 'Not found' })
|
||||
when ({ status: status if status >= 500 }) -> ({ success: false, error: 'Server error' })
|
||||
default -> ({ success: false, error: 'Unknown error' })
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Array Length Branches Gracefully
|
||||
|
||||
```javascript
|
||||
function handleArray(arr) {
|
||||
return match (arr) {
|
||||
when ([]) -> "Empty array"
|
||||
when ([first]) -> `Only one element: ${first}`
|
||||
when ([first, second]) -> `Two elements: ${first}, ${second}`
|
||||
when ([first, ...rest]) -> `First element: ${first}, others: ${rest.length} items`
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Email Like a Boss
|
||||
tags:
|
||||
- communication
|
||||
- email
|
||||
- soft-skill
|
||||
- career
|
||||
- productivity
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
Email at work is about more than sharing information — it's about building trust and shaping your professional image.
|
||||
The same message, phrased differently, can leave an entirely different impression.
|
||||
|
||||
Below are 9 common email scenarios, contrasting "low-power" expressions with "high-power" alternatives that make you
|
||||
sound more confident and professional.
|
||||
|
||||
## Scenario Cheatsheet
|
||||
|
||||
| Scenario | ❌ Don't Use (sounds…) | ✅ Use Instead (sounds…) | Why It Works |
|
||||
|----------------------------|----------------------------------------------|-----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Late reply | **Sorry for the delay** | **Thanks for your patience** | Swaps an apology for gratitude — the focus shifts from "I was wrong" to "you were generous," acknowledging the delay while making the recipient feel respected |
|
||||
| Scheduling | **What works best for you?** | **Could you do …?** | The former throws the decision entirely back to the other person; the latter offers a concrete option, cutting down on back-and-forth |
|
||||
| After helping someone | **No problem / No worries** | **Always happy to help** | The former implies the task *could* have been a problem; the latter signals you enjoyed it and would happily do it again |
|
||||
| Making a suggestion | **I think maybe we should …** | **It'd be best if we …** | The former oozes hesitation and self-doubt; the latter delivers a clear judgment — like someone with experience making a decision |
|
||||
| Text isn't working | **_Spending 30 minutes rewriting an email_** | **It'd be easier to discuss in person** | Recognising the medium itself is the bottleneck and switching channels can be the most efficient move |
|
||||
| Checking for understanding | **Hopefully that makes sense?** | **Let me know if you have questions** | The former betrays doubt about your own clarity; the latter calmly shares responsibility — the reader now has an action item too |
|
||||
| Following up on progress | **Just wanted to check in** | **When can I expect an update?** | The former tiptoes around the ask; the latter names the time frame directly — clear, polite, and professional |
|
||||
| Owned a small mistake | **Ahh sorry my bad totally missed that** | **Thanks for letting me know** | Over-apologising makes things awkward; this acknowledges the catch while keeping the focus on moving forward |
|
||||
| Need to leave early | **Could I possibly leave early?** | **I will need to leave at …** | The former asks for permission; the latter states a plan — you're a professional and don't need to apologise for reasonable needs |
|
||||
|
||||
## Core Principles
|
||||
|
||||
Writing great emails is less about vocabulary and more about **stance**. Keep three rules in mind:
|
||||
|
||||
1. **State instead of ask** — "I need…" carries more weight than "Could I possibly…"
|
||||
2. **Thank instead of apologise** — Shift the focus from "my shortcoming" to "their support"
|
||||
3. **Be specific instead of vague** — Offer exact times, options, and action items rather than lobbing the ball back
|
||||
into their court
|
||||
|
||||
Next time you open your inbox, take five seconds to ask: can I phrase this more like someone who makes decisions?
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Fix macOS Monterey+ Devices Waking Frequently from Sleep
|
||||
tags:
|
||||
- macos
|
||||
- sleep
|
||||
- power-management
|
||||
- bug
|
||||
---
|
||||
|
||||
> This article was originally written by **落格博客
|
||||
**: [落格博客](https://www.logcg.com/) » [Frequent Wake-from-Sleep Issues After Upgrading to macOS Monterey](https://www.logcg.com/archives/3528.html)
|
||||
|
||||
After upgrading to macOS Monterey, my screen kept lighting up in the middle of the night for no apparent reason. It had
|
||||
happened before, but only when notifications came in. Now the screen lights up on its own with no trigger — same
|
||||
hardware, so it must be a software issue.
|
||||
|
||||
After searching online, I first
|
||||
found [Apple's official guide](https://support.apple.com/en-gb/guide/mac-help/mchlp2995/mac). It's very detailed, but
|
||||
clearly of no help whatsoever.
|
||||
|
||||
Digging deeper, I found the root cause. Run **`pmset -g log | grep DarkWake`** and you'll see your Mac hasn't been
|
||||
resting while you slept...
|
||||
|
||||
Several typical patterns appear, most with a DarkWake immediately followed by a Wake. The issue: DarkWake is meant to
|
||||
wake the computer in the background to update data, but somehow a peripheral gets triggered, causing a full system wake.
|
||||
|
||||
In any case, I don't want this feature — Power Nap. For me, I'd rather it save as much power as possible. The fix path:
|
||||
disable network access wake, disable Power Nap... But here's the catch — on M1 devices, there is no Power Nap option in
|
||||
settings. (Clearly, Apple is confident in their battery life but overlooked the power of bugs.)
|
||||
|
||||
So for Power Nap, we have to go through the command line. First, check the current status with **`pmset -g`**. Find the
|
||||
**`powernap`** value — if it isn't 0, it's enabled. Disable it with **`sudo pmset -a powernap 0`**.
|
||||
|
||||
Also check **`tcpkeepalive`** — it likely isn't 0 either and should also be turned off. It controls whether your Mac
|
||||
maintains TCP connections while sleeping. Run **`sudo pmset -a tcpkeepalive 0`** — you'll see a terminal warning:
|
||||
***Warning: This option disables TCP Keep Alive mechanism when system is sleeping. This will result in some critical
|
||||
features like 'Find My Mac' not to function properly.***
|
||||
|
||||
Essentially, turning it off limits some features — the system simply won't connect to the network while asleep. I'm
|
||||
fairly confident that if someone actually steals your Mac, they won't be getting it online anyway.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Font Size Conversion Table
|
||||
tags:
|
||||
- typography
|
||||
- font
|
||||
- cheatsheet
|
||||
- design
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
| Chinese Size Name | English Point Size (pt) | mm | px |
|
||||
|--------------------|-------------------------|-------|------|
|
||||
| 初号 (Primary) | 42 | 14.82 | 56 |
|
||||
| 小初 (Small Primary) | 36 | 12.7 | 48 |
|
||||
| 一号 (No. 1) | 26 | 9.17 | 34.7 |
|
||||
| 小一 (Small No. 1) | 24 | 8.47 | 32 |
|
||||
| 二号 (No. 2) | 22 | 7.76 | 29.3 |
|
||||
| 小二 (Small No. 2) | 18 | 6.35 | 24 |
|
||||
| 三号 (No. 3) | 16 | 5.64 | 21.3 |
|
||||
| 小三 (Small No. 3) | 15 | 5.29 | 20 |
|
||||
| 四号 (No. 4) | 14 | 4.94 | 18.7 |
|
||||
| 小四 (Small No. 4) | 12 | 4.23 | 16 |
|
||||
| 五号 (No. 5) | 10.5 | 3.7 | 14 |
|
||||
| 小五 (Small No. 5) | 9 | 3.18 | 12 |
|
||||
| 六号 (No. 6) | 7.5 | 2.56 | 10 |
|
||||
| 小六 (Small No. 6) | 6.5 | 2.29 | 8.7 |
|
||||
| 七号 (No. 7) | 5.5 | 1.94 | 7.3 |
|
||||
| 八号 (No. 8) | 5 | 1.76 | 6.7 |
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Frontend Development Standards
|
||||
tags:
|
||||
- frontend
|
||||
- react
|
||||
- standards
|
||||
- best-practice
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
## Dependency Management (pnpm)
|
||||
|
||||
- **Strictness**: Leverage pnpm's strict dependency management to ensure a more deterministic `node_modules` structure
|
||||
and efficient disk space usage.
|
||||
- **Workspaces**: When using a monorepo approach, configure pnpm workspaces to streamline dependency management across
|
||||
multiple frontend packages.
|
||||
- **Auditing**: Regularly audit frontend dependencies for known vulnerabilities using `pnpm audit`.
|
||||
|
||||
## API Communication (Axios)
|
||||
|
||||
- **Axios Instance**: Create a centralised Axios instance for API calls to apply common configuration (base URL,
|
||||
headers, interceptors).
|
||||
- **Interceptors**: Use Axios interceptors to:
|
||||
- Add authentication tokens to outgoing requests.
|
||||
- Handle global error responses (e.g., display a notification for `401 Unauthorized`).
|
||||
- Log requests/responses in development environments.
|
||||
- **Error Handling**: Centralise API error handling in Axios interceptors or custom utility functions to provide
|
||||
consistent user feedback.
|
||||
|
||||
## React and Component Standards
|
||||
|
||||
- **Function Components & Hooks**: Prefer function components with React Hooks over class components for new
|
||||
development.
|
||||
- **Props**:
|
||||
- Define `interface` or `type` for component props to ensure type safety.
|
||||
- Destructure props at the component entry point for clarity.
|
||||
- **State Management (Redux)**:
|
||||
- Use Redux Toolkit for efficient, boilerplate-reduced Redux development.
|
||||
- Use `createSlice` to organise Redux logic into "slices" (feature-specific reducers, actions, and selectors).
|
||||
- Follow the "ducks" pattern or "slices" approach to co-locate Redux logic.
|
||||
- **Component Composition**: Break down complex UIs into smaller, reusable, single-responsibility components.
|
||||
- **Ant Design**:
|
||||
- Leverage Ant Design components for consistent UI/UX.
|
||||
- Use CSS-in-JS solutions to consistently customise Ant Design themes and styles across the application if needed.
|
||||
- **Accessibility**: Design and implement components with web accessibility (a11y) in mind from the start.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Frontend Tips and Solutions Cheatsheet
|
||||
tags:
|
||||
- frontend
|
||||
- react
|
||||
- ant-design
|
||||
- tailwind
|
||||
- best-practice
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
## Decoupling Form Components
|
||||
|
||||
Split form components into UI components and logic components. Use the UI components inside the logic components for rendering styles.
|
||||
|
||||
## React Entry Component Order
|
||||
|
||||
```typescript
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { Provider as ReduxProvider } from "react-redux"
|
||||
import { PersistGate } from "redux-persist/integration/react"
|
||||
import { RouterProvider } from "react-router"
|
||||
import { App as AntApp, ConfigProvider as AntConfigProvider } from "antd"
|
||||
import { StyleProvider } from "@ant-design/cssinjs"
|
||||
import AntSimplifiedChinese from "antd/locale/zh_CN"
|
||||
import "./index.css"
|
||||
import "@/config/dayjs-config"
|
||||
import store, { persistor } from "@/store"
|
||||
import router from "@/router"
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ReduxProvider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
{/* Note: StyleProvider must be the parent of ConfigProvider!!! */}
|
||||
<StyleProvider layer>
|
||||
<AntConfigProvider
|
||||
locale={AntSimplifiedChinese}
|
||||
button={{
|
||||
autoInsertSpace: false,
|
||||
}}>
|
||||
<AntApp className="h-full w-full">
|
||||
<RouterProvider router={router} />
|
||||
</AntApp>
|
||||
</AntConfigProvider>
|
||||
</StyleProvider>
|
||||
</PersistGate>
|
||||
</ReduxProvider>
|
||||
</StrictMode>
|
||||
)
|
||||
```
|
||||
|
||||
## Integrating Ant Design with Tailwind CSS in React
|
||||
|
||||
> Reference: [https://github.com/ant-design/ant-design/discussions/56152](https://github.com/ant-design/ant-design/discussions/56152)
|
||||
|
||||
```css
|
||||
/* index.css */
|
||||
|
||||
@layer theme, base, antd, components, utilities;
|
||||
@import "../../../node_modules/.pnpm/tailwindcss@4.3.0/node_modules/tailwindcss/dist/lib.d.mts";
|
||||
```
|
||||
|
||||
```typescript
|
||||
<StyleProvider>
|
||||
<ConfigProvider>
|
||||
<App>
|
||||
<RouterProvider/>
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
</StyleProvider>
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: General Application Development Standards
|
||||
tags:
|
||||
- standards
|
||||
- best-practice
|
||||
- engineering
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
This document outlines coding standards and best practices for application development. Adhering to these guidelines
|
||||
ensures code quality, maintainability, and consistency across all projects.
|
||||
|
||||
## General Principles
|
||||
|
||||
- **Clarity & Readability**: Code must be easy to read and understand. Prefer clear, self-documenting code over clever,
|
||||
terse, but obscure solutions.
|
||||
- **Consistency**: Maintain a consistent coding style, naming conventions, and architectural patterns across the
|
||||
project.
|
||||
- **Modularity**: Design components with loose coupling and high cohesion to promote reusability and simpler testing.
|
||||
- **Testability**: Write code that is inherently testable.
|
||||
- **Security-First Design**: Incorporate security considerations at every stage of development.
|
||||
- **Performance Awareness**: Be mindful of performance implications for critical code paths and API endpoints.
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: GitLab Operations
|
||||
tags:
|
||||
- gitlab
|
||||
- devops
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
## Setting the Default Language to Chinese in GitLab
|
||||
|
||||
GitLab filters out languages with less than 90% translation coverage via the `SWITCHER_MINIMUM_TRANSLATION_LEVEL`
|
||||
variable in the code. Simplified Chinese sits at 84% and Traditional Chinese at 83%.
|
||||
|
||||
Navigate to `/path/to/gitlab/embedded/service/gitlab-rails/app/helpers/preferred_language_switcher_helper.rb` and modify
|
||||
the code as follows:
|
||||
|
||||
```diff title="/path/to/gitlab/embedded/service/gitlab-rails/app/helpers/preferred_language_switcher_helper.rb"
|
||||
- SWITCHER_MINIMUM_TRANSLATION_LEVEL = 90
|
||||
+ SWITCHER_MINIMUM_TRANSLATION_LEVEL = 80
|
||||
```
|
||||
|
||||
Then restart GitLab with `gitlab-ctl restart`.
|
||||
|
||||
## Adding ICP Filing Information to the Homepage
|
||||
|
||||
Navigate to `/path/to/gitlab/embedded/service/gitlab-rails/app/views/devise/shared/_footer.html.haml` and add the
|
||||
following:
|
||||
|
||||
```diff
|
||||
+ = link_to _("Your ICP Filing Number"), "https://beian.miit.gov.cn/", target: '_blank', class: 'text-nowrap', rel: 'noopener noreferrer'
|
||||
```
|
||||
|
||||
## Upgrading GitLab
|
||||
|
||||
### Downloading the GitLab CE Package
|
||||
|
||||
Download the deb package from the [GitLab Package page](https://packages.gitlab.com/gitlab/gitlab-ce) and transfer it to
|
||||
the server using `scp` or a similar tool.
|
||||
|
||||
### Stopping Memory-Intensive Services
|
||||
|
||||
```shell
|
||||
gitlab-ctl stop puma
|
||||
gitlab-ctl stop sidekiq
|
||||
```
|
||||
|
||||
### Running a Backup
|
||||
|
||||
```shell
|
||||
# Example: skip build artefacts and container registry, backing up only the database and repositories
|
||||
sudo gitlab-backup create SKIP=artifacts,registry
|
||||
```
|
||||
|
||||
### Installing
|
||||
|
||||
```shell
|
||||
sudo dpkg -i gitlab-package.deb
|
||||
```
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
title: Google Code Review Standards
|
||||
tags:
|
||||
- code-review
|
||||
- best-practice
|
||||
- engineering
|
||||
- google
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
Code review is a step in the development process where one or more developers review code written by another developer (
|
||||
the author) to ensure that:
|
||||
|
||||
- The code is free of errors, bugs, and issues;
|
||||
- The code conforms to quality and style guide requirements and standards;
|
||||
- The code fulfils all intended functionality;
|
||||
- Once merged, the codebase continues to function properly and is left in a better state.
|
||||
|
||||
This is why code review is a vital part of software development. Code reviewers act as gatekeepers, responsible for
|
||||
deciding whether the code is ready to become part of the codebase and enter production.
|
||||
|
||||
## The Code Should Improve the Overall Health of the System
|
||||
|
||||
Every code change (pull request) should improve the overall health of the system. The key point is that even small
|
||||
improvements, once merged, enhance the health of the software or codebase.
|
||||
|
||||
## Review Code Quickly and Provide Active Responses and Feedback
|
||||
|
||||
First and foremost, do not delay the merging of code. There is no such thing as perfect code. If the code improves the
|
||||
overall health of the system, it should be delivered promptly.
|
||||
|
||||
> The key insight is that there is no perfect code — only better code. — Google Engineering Practices Documentation
|
||||
|
||||
If there are no urgent tasks at hand, review code as soon as it is submitted. The maximum response time for a pull
|
||||
request should not exceed one working day. Multiple rounds of partial or complete code review should be completed for a
|
||||
single pull request within a day.
|
||||
|
||||
## Educate and Inspire During Code Review
|
||||
|
||||
During code review, provide guidance by sharing knowledge and experience wherever possible.
|
||||
|
||||
## Review Code Against Standards
|
||||
|
||||
Always remember that style guides, coding standards, and relevant documentation should serve as the absolute authority
|
||||
in code review. For example, when consistency of tabs versus spaces is in question, cite the coding conventions.
|
||||
|
||||
> If you use Java, the following article may be helpful — it summarises Java coding best practices from major tech
|
||||
> companies: [A Short Summary of Java Coding Best Practices](https://rhamedy.medium.com/a-short-summary-of-java-coding-best-practices-31283d0167d3)
|
||||
|
||||
## Resolving Code Review Conflicts
|
||||
|
||||
When resolving conflicts during code review, follow the agreed best practices in the style guide and coding standards,
|
||||
and seek advice from others with more domain knowledge and experience.
|
||||
|
||||
If your comment is optional or relatively unimportant, indicate so in the comment and let the author decide whether to
|
||||
address or skip it.
|
||||
|
||||
As a reviewer, when there is no style guide or coding standard to reference, you can at minimum suggest that the code
|
||||
change remain consistent with the rest of the codebase.
|
||||
|
||||
## UI Change Demonstrations Are Part of Code Review
|
||||
|
||||
If a code change involves user interface modifications, a demonstration is required in addition to the code review to
|
||||
ensure the UI meets expectations and aligns with the interface design.
|
||||
|
||||
For frontend code changes, you must provide a demonstration or ensure that the code change includes the necessary UI
|
||||
automation tests to verify the added or updated functionality.
|
||||
|
||||
## Ensure All Tests Are Included in the Code Review
|
||||
|
||||
Unless in an emergency, pull requests should include all necessary tests, such as unit tests, integration tests, and
|
||||
end-to-end tests.
|
||||
|
||||
An emergency here means a bug or security vulnerability that needs fixing as soon as possible, and tests can be added
|
||||
later. In such cases, ensure that appropriate tickets/issues are created and someone is responsible for completing the
|
||||
tests immediately after the hotfix or deployment.
|
||||
|
||||
Skipping tests must never be accepted. If time is short and certain goals risk not being met, the solution is not to
|
||||
skip tests but to scope down the deliverables.
|
||||
|
||||
## Don't Interrupt Your Own Work for Code Review
|
||||
|
||||
If you are deeply focused on your work, don't interrupt yourself — it takes a long time to get back into the flow. In
|
||||
other words, the cost of interrupting a developer in flow far exceeds the cost of making them wait for a code review. Do
|
||||
your code reviews after a break (lunch, coffee, etc.).
|
||||
|
||||
Most of the time, an entire code review and merge cannot be completed in a single day. What matters is giving the author
|
||||
prompt feedback. For example, even if you cannot complete a full review, you can quickly point out a few areas worth
|
||||
discussing. This significantly reduces frustration during the review process.
|
||||
|
||||
## Review All Code — Make No Assumptions
|
||||
|
||||
Review every line of code that is submitted. Do not make assumptions about manually written classes and methods, and
|
||||
make sure you understand what the code is doing.
|
||||
|
||||
Make sure you understand the code you are reviewing. If you don't, ask the author for clarification or a code
|
||||
walkthrough and explanation. If you are not qualified to review part of the code, ask another qualified developer to
|
||||
review it in your place.
|
||||
|
||||
## Keep the Big Picture in Mind During Code Review
|
||||
|
||||
It helps to look at code changes from a broader perspective. For example, a file is modified and four new lines of code
|
||||
are added. Don't just look at those four lines — consider reviewing the entire file and examining what was added. Do the
|
||||
additions degrade the quality of existing code? Do they make existing functionality a candidate for refactoring?
|
||||
|
||||
Reviewing added code outside the context of the function/method or class will, over time, lead to classes that are
|
||||
unmaintainable, tangled, difficult to test, and hard to extend or refactor.
|
||||
|
||||
Remember that just as trivial improvements can compound into a better product over time, even minor code degradation or
|
||||
technical debt can accumulate to the point where the product becomes difficult to maintain and extend.
|
||||
|
||||
## Recognise and Encourage Great Work During Code Review
|
||||
|
||||
If you see an excellent code change, don't forget to acknowledge and encourage the author generously. The purpose of
|
||||
code review is not only to find errors but also to encourage and guide developers towards great work.
|
||||
|
||||
## Be Considerate, Respectful, Kind, and Clear During Code Review
|
||||
|
||||
It is critical to remain kind, clear, polite, and respectful during code review, while also providing the author with
|
||||
clear feedback and positive assistance. When reviewing code, comment on the code — not the developer.
|
||||
|
||||
## Explain Code Review Comments Thoroughly, with a Sense of Proportion
|
||||
|
||||
Whenever a code review comment proposes an alternative or points out a problem, it's important to explain the reasoning
|
||||
and provide examples based on your knowledge and experience to help the developer understand why your suggestion
|
||||
improves the code quality.
|
||||
|
||||
When suggesting modifications or changes, find the right balance in how you guide the author. For example, I prefer
|
||||
guidance, explanation, hints, or suggestions over providing the entire solution.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Blog
|
||||
---
|
||||
|
||||
# Blog
|
||||
|
||||
Welcome to the OnixByte blog — a collection of practical guides, cheatsheets, and standards
|
||||
distilled from daily engineering work.
|
||||
|
||||
## Topics
|
||||
|
||||
- **Frontend** — React patterns, TailwindCSS tips, ECMAScript syntax guides, browser quirks
|
||||
- **Backend & Database** — Java development, MyBatis, PostgreSQL, MySQL JSON
|
||||
- **DevOps & Infrastructure** — Docker, GitLab CI/CD, LDAP, MinIO, macOS troubleshooting
|
||||
- **Engineering Standards** — code review, version control, general and language-specific conventions
|
||||
- **Tools & Productivity** — email etiquette, font sizing, data analysis methods
|
||||
|
||||
Browse the sidebar or scroll through the posts below — each article is self-contained and written to be immediately useful.
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
title: Java Development Cheatsheet
|
||||
tags:
|
||||
- java
|
||||
- tips
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
## Comparison for `BigDecimal`
|
||||
|
||||
In Java, comparing `BigDecimal` values requires care because `equals` and `compareTo` behave differently.
|
||||
|
||||
### `equals` vs `compareTo`
|
||||
|
||||
`BigDecimal#equals` checks both **value** and **scale**, while `BigDecimal#compareTo` only checks **value** (ignoring scale). This means:
|
||||
|
||||
```java
|
||||
var a = new BigDecimal("100"); // scale = 0
|
||||
var b = new BigDecimal("100.00"); // scale = 2
|
||||
|
||||
a.equals(b); // false — different scale
|
||||
a.compareTo(b); // 0 — same mathematical value
|
||||
|
||||
var c = new BigDecimal("200");
|
||||
|
||||
a.compareTo(c); // -1 (negative) — a is less than c
|
||||
c.compareTo(a); // 1 (positive) — c is greater than a
|
||||
```
|
||||
|
||||
### Why this matters
|
||||
|
||||
The scale mismatch often appears when values come from different sources — e.g., parsing user input, reading from a database (`DECIMAL(10,2)` columns), or receiving JSON payloads. You might think two values are equal when `equals` says they aren't.
|
||||
|
||||
### Rule of thumb
|
||||
|
||||
- Use **`compareTo`** for numeric equality checks: `a.compareTo(b) == 0`
|
||||
- Use **`equals`** only when you mean "identical representation" (same value and same scale)
|
||||
- Use **`stripTrailingZeros()`** if you need to normalise scale before `equals`:
|
||||
|
||||
```java
|
||||
a.stripTrailingZeros().equals(b.stripTrailingZeros()); // true
|
||||
```
|
||||
|
||||
### Comparing with zero
|
||||
|
||||
Avoid `==` or `.equals(BigDecimal.ZERO)` to check for zero — prefer `compareTo`:
|
||||
|
||||
```java
|
||||
if (value.compareTo(BigDecimal.ZERO) == 0) { ... }
|
||||
```
|
||||
|
||||
## How to Retrieve Data from a BlockingQueue
|
||||
|
||||
- `take()` — retrieves and removes the head of the queue, waiting if necessary until an element becomes available.
|
||||
- `poll()` — retrieves and removes the head of the queue, or returns `null` if the queue is empty.
|
||||
- `poll(long timeout, TimeUnit unit)` — retrieves and removes the head of the queue, waiting up to the specified wait time if necessary for an element to become available. Returns `null` if the timeout expires.
|
||||
- `peek()` — retrieves but does not remove the head of the queue. Returns `null` if the queue is empty.
|
||||
|
||||
## Spring Cloud Alibaba FAQs
|
||||
|
||||
### How to prevent Nacos from creating a `nacos` folder in the user's home directory?
|
||||
|
||||
Add the following two configuration properties to specify the Nacos storage path:
|
||||
|
||||
- `JM.LOG.PATH`
|
||||
- `JM.SNAPSHOT.PATH`
|
||||
|
||||
### How to deal with Sentinel's scattered log files?
|
||||
|
||||
Add the configuration property `csp.sentinel.log.dir` to change Sentinel's log directory.
|
||||
|
||||
### How to add Configuration Properties in JetBrains IntelliJ IDEA?
|
||||
|
||||
In JetBrains IntelliJ IDEA, click **`Edit Configurations…`** in the run configuration dropdown at the top right.
|
||||
|
||||
Click the **`Modify options`** button on the page, then add the properties you need to reset in the **`Override configuration properties`** table that appears below.
|
||||
|
||||
## Spring Data JPA FAQs
|
||||
|
||||
### How to fix the "Serializing `PageImpl` instances as-is not supported" warning?
|
||||
|
||||
Spring Data JPA warns about unstable JSON serialization of `PageImpl`. To resolve this, enable VIA_DTO serialization mode on your application's main class:
|
||||
|
||||
```java
|
||||
@EnableSpringDataWebSupport(pageSerializationMode =
|
||||
EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
|
||||
```
|
||||
|
||||
### Why does the first page return no results?
|
||||
|
||||
JPA pagination is **zero-indexed**. Page `0` is the first page. If your frontend sends `page=1`, you need to pass `page - 1` to Spring Data:
|
||||
|
||||
```java
|
||||
Pageable pageable = PageRequest.of(requestPage - 1, pageSize);
|
||||
```
|
||||
|
||||
### How to avoid the N+1 query problem?
|
||||
|
||||
The N+1 problem occurs when JPA executes one query for the parent entity, then N additional queries for each child association.
|
||||
|
||||
**Detection** — look for repetitive SQL queries in the logs, or configure `spring.jpa.properties.hibernate.generate_statistics=true` to spot it.
|
||||
|
||||
**Fixes:**
|
||||
|
||||
| Approach | When to use |
|
||||
|--------------------------|---------------------------------------------------|
|
||||
| `@EntityGraph` | Declarative, good for entity-specific fetch plans |
|
||||
| `JOIN FETCH` in `@Query` | Fine-grained control per query |
|
||||
| `@BatchSize` | Reduces N+1 to N/k+1 by batching |
|
||||
|
||||
```java
|
||||
// Option 1: EntityGraph
|
||||
@EntityGraph(attributePaths = {"roles", "permissions"})
|
||||
Optional<User> findById(long id);
|
||||
|
||||
// Option 2: JOIN FETCH
|
||||
@Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id")
|
||||
Optional<User> findByIdWithRoles(@Param("id") long id);
|
||||
```
|
||||
|
||||
### `findById` vs `getReferenceById` — which one to use?
|
||||
|
||||
- **`findById`** — hits the database immediately, returns the entity or `Optional.empty()`. Use this when you need the data.
|
||||
- **`getReferenceById`** — returns a lazy proxy **without** hitting the database. Throws `EntityNotFoundException` only when you access a non-existent proxy's properties. Use this when you only need the ID to set a foreign key relationship.
|
||||
|
||||
```java
|
||||
// Good: only need the user reference to set a FK
|
||||
Post post = new Post();
|
||||
post.setAuthor(userRepository.getReferenceById(userId));
|
||||
```
|
||||
|
||||
### How to fix `LazyInitializationException`?
|
||||
|
||||
This happens when you access a lazily-loaded association outside the persistence context (e.g., in a controller or serializer after the transaction has closed).
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Use `JOIN FETCH` or `@EntityGraph`** to eagerly load needed associations.
|
||||
2. **Use DTO projections** — return only the fields you need instead of whole entities:
|
||||
|
||||
```java
|
||||
@Query("SELECT new com.example.UserDto(u.id, u.name) FROM User u WHERE u.id = :id")
|
||||
UserDto findUserDtoById(@Param("id") long id);
|
||||
```
|
||||
3. **`@Transactional(readOnly = true)`** on the service method — keep the session open for the entire method scope.
|
||||
|
||||
### When should I use `@Transactional(readOnly = true)`?
|
||||
|
||||
Use `@Transactional(readOnly = true)` on **read-only** service methods for three benefits:
|
||||
|
||||
- Hibernate skips dirty checking (no snapshots, less memory).
|
||||
- The JDBC driver may route to read replicas.
|
||||
- It documents the intent clearly.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public UserDto getUser(long id) { ... }
|
||||
|
||||
@Transactional
|
||||
public UserDto createUser(CreateUserRequest request) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### `save()` vs `saveAll()` — which is faster for batch inserts?
|
||||
|
||||
`saveAll()` uses a single transaction and can benefit from JDBC batching. Configure the batch size:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
jpa:
|
||||
properties:
|
||||
hibernate:
|
||||
jdbc:
|
||||
batch_size: 20
|
||||
order_inserts: true
|
||||
order_updates: true
|
||||
```
|
||||
|
||||
For large bulk inserts (thousands of rows), consider `JdbcTemplate` batch operations instead — Hibernate's entity management overhead is significant at that scale.
|
||||
|
||||
### How to use dynamic queries with `Specification`?
|
||||
|
||||
For complex search forms with optional filters, use `JpaSpecificationExecutor`:
|
||||
|
||||
```java
|
||||
public interface UserRepository extends JpaRepository<User, Long>,
|
||||
JpaSpecificationExecutor<User> {
|
||||
}
|
||||
|
||||
// Usage
|
||||
Specification<User> spec = (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
if (name != null) {
|
||||
predicates.add(cb.like(root.get("name"), "%" + name + "%"));
|
||||
}
|
||||
if (status != null) {
|
||||
predicates.add(cb.equal(root.get("status"), status));
|
||||
}
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
|
||||
Page<User> page = userRepository.findAll(spec, pageable);
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
---
|
||||
title: Java Development Standards
|
||||
tags:
|
||||
- java
|
||||
- spring-boot
|
||||
- standards
|
||||
- best-practice
|
||||
- backend
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
## Java Language and Coding Style
|
||||
|
||||
- **Java Version**: Projects should use the latest LTS version of the JDK whenever possible.
|
||||
- **Naming Conventions**:
|
||||
- Classes: `PascalCase` (e.g., `UserService`, `OrderController`).
|
||||
- Methods: `camelCase` (e.g., `getUserById`, `saveOrder`).
|
||||
- Variables: `camelCase` (e.g., `username`, `statusCode`).
|
||||
- Constants: `SCREAMING_SNAKE_CASE` (e.g., `DEFAULT_PAGE_SIZE`).
|
||||
- **Immutability**: Prefer immutability for domain objects and DTOs where possible, using `records` or immutable classes to reduce side effects and improve thread safety.
|
||||
- **Optional**: Use `Optional<T>` to explicitly handle potentially absent values and avoid `NullPointerException`.
|
||||
- **Streams API**: Prefer the Java Streams API for collection processing, promoting functional and declarative programming.
|
||||
- **Exception Handling**:
|
||||
- Use specific exceptions. Avoid catching generic `Exception`.
|
||||
- Throw runtime exceptions for unrecoverable errors.
|
||||
- Define custom checked exceptions for recoverable errors if required by business logic.
|
||||
- Leverage Spring's `@ControllerAdvice` and `@ExceptionHandler` for centralised global exception handling and consistent API error responses.
|
||||
- **Code Review**: All backend code must undergo thorough manual code review before merging, particularly following GitFlow principles. IntelliJ IDEA's integrated code analysis tools should be used as a first-pass review.
|
||||
|
||||
## Documentation and Comments
|
||||
|
||||
- All **public** classes, methods, and significant fields in backend Java code must include comprehensive Javadoc comments.
|
||||
- Javadoc should explain the purpose, parameters (`@param`), return values (`@return`), and thrown exceptions (`@throws`).
|
||||
- Javadoc formatting:
|
||||
- Javadoc must follow a maximum of 100 characters per line (including whitespace for formatting). If the content exceeds 100 characters, break at the last word that ends within the 100-character limit. However, if after the line break only a single word remains at the start of the next line, break one word earlier.
|
||||
|
||||
```java
|
||||
/**
|
||||
* Enables configuration properties for S3 file storage services. Individual service beans are
|
||||
* created by their respective service classes to better support conditional configuration.
|
||||
*/
|
||||
```
|
||||
|
||||
- Use `<p>` to separate paragraphs.
|
||||
|
||||
```java
|
||||
/**
|
||||
* This is the first paragraph of the Javadoc.
|
||||
* <p>
|
||||
* This is the second paragraph of the Javadoc.
|
||||
*/
|
||||
```
|
||||
|
||||
- Each paragraph must be a grammatically correct and semantically complete paragraph following English sentence conventions.
|
||||
- All `@param`, `@return`, `@throws`, and `@see` explanations must follow these rules:
|
||||
- Do not start with a capital letter.
|
||||
- If the description ends with a declarative sentence, do not use punctuation at the end.
|
||||
|
||||
```java
|
||||
/**
|
||||
* Returns the greater of two {@code int} values. That is, the
|
||||
* result is the argument closer to the value of
|
||||
* {@link Integer#MAX_VALUE}. If the arguments have the same value,
|
||||
* the result is that same value.
|
||||
*
|
||||
* @param a an argument
|
||||
* @param b another argument
|
||||
* @return the larger of {@code a} and {@code b}
|
||||
*/
|
||||
public static int max(int a, int b) {
|
||||
return (a >= b) ? a : b;
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Management (Gradle)
|
||||
|
||||
- **Build File**: `build.gradle.kts` must be well-organised with clear separation of plugins, dependencies, and tasks.
|
||||
- **Dependency Versions**: Dependency versions must be centrally managed in the `gradle/libs.versions.toml` file to ensure consistency.
|
||||
- **Plugin Management**: Explicitly declare Gradle plugins and their versions.
|
||||
- **Avoid Unnecessary Dependencies**: Only include dependencies actually used by the project. Regularly review and clean up unused dependencies.
|
||||
|
||||
## API Design (RESTful)
|
||||
|
||||
- **RESTful Principles**: Follow RESTful principles:
|
||||
- **Resources**: Model data as resources identifiable by URI.
|
||||
- **HTTP Methods**: Use standard HTTP methods appropriately (GET for retrieval, POST for creation, PUT for full update, PATCH for partial update, DELETE for removal).
|
||||
- **Statelessness**: APIs should be stateless; each request from client to server must contain all the information needed to understand the request.
|
||||
- **URIs**:
|
||||
- Use plural nouns for collection resources (e.g., `/users`, `/products`).
|
||||
- Use hyphens in URIs for readability (e.g., `/user-accounts`).
|
||||
- Avoid verbs in URIs (e.g., use `/users` instead of `/getAllUsers`).
|
||||
- **Status Codes**: Use appropriate HTTP status codes to indicate the result of API requests (e.g., `200 OK`, `201 Created`, `204 No Content`, `400 Bad Request`, `401 Unauthorized`, `403 Forbidden`, `404 Not Found`, `500 Internal Server Error`).
|
||||
- **Response Format**: JSON is the preferred response format.
|
||||
- **Versioning**: Where version control is required, use the `X-Endpoint-Version` header parameter to control API versions.
|
||||
|
||||
## Spring Boot Best Practices & Layered Architecture
|
||||
|
||||
- **Layered Architecture (MVC with Manager Layer)**: Our backend applications follow a strict multi-layered architecture, ensuring clear separation of responsibilities and improving maintainability and testability. The layers and their responsibilities are:
|
||||
|
||||
- **Controller Layer**: Located in the `controller` package. Responsible for exposing RESTful APIs, handling HTTP requests, and mapping request parameters/bodies to service layer calls. Controllers should remain lightweight, focusing primarily on input validation (using DTOs) and coordinating calls to the `Service` layer.
|
||||
- **Service Layer**: Located in the `service` package. This layer encapsulates core business logic. Services expose a directly consumable API to the `Controller` layer, abstracting business processes and transaction management. Services coordinate calls to the `Manager` layer to execute business operations.
|
||||
- **Manager Layer**: Located in the `manager` package. This layer provides atomic business operations that can be composed by the `Service` layer. Managers typically handle more complex business logic and may involve interacting with multiple repositories or other external systems at a finer granularity.
|
||||
- **Repository Layer (MyBatis)**: Located in the `repository` package and `src/main/resources/repository` (for XML mapping files). This layer is responsible for providing atomic database interaction operations.
|
||||
|
||||
**Inter-Layer Communication Strategy**:
|
||||
- **Components within a layer may only call components in the layer directly below it.**
|
||||
- **Cross-layer calls are strictly prohibited** (e.g., Controller directly calling Manager, Service directly calling Repository).
|
||||
- **Upward calls are strictly prohibited** (e.g., Service calling Controller).
|
||||
- **Lateral calls** (e.g., Service A calling Service B for a different domain) should be carefully considered and typically indicate a need to refactor shared logic into a `Manager` or a dedicated `Service` for that shared concern.
|
||||
|
||||
- **Configuration**: Prefer `application.yml` over `application.properties` for configuration properties, offering better readability and hierarchical structure. Use `@ConfigurationProperties` for type-safe configuration.
|
||||
- **Dependency Injection**: All dependencies should use constructor injection (mandatory dependencies) or setter injection (optional dependencies). Avoid `@Autowired` on fields, as it makes testing more difficult and hides dependencies.
|
||||
- **Services**: Business logic classes are annotated with `@Service`. Services should be kept lean and focused on orchestrating domain operations, typically involving business logic processing through `Manager` layer components and interaction through `Manager` or directly with MyBatis repositories (if the particular operation does not require intermediate Manager logic, though the Manager layer is preferred for all repository interactions consistent with the layered architecture definition).
|
||||
|
||||
- **Repositories (MyBatis & JPA)**:
|
||||
- **The project uses both MyBatis and JPA for database operations**. Repository interfaces in the `repository` and `mapper` packages define the data access contract.
|
||||
- Corresponding SQL definitions are managed in XML mapping files located in `src/main/resources/mapper`.
|
||||
- Data operation conventions:
|
||||
- Use JPA for simple database operations.
|
||||
- Use MyBatis for complex database operations.
|
||||
- When performing paginated queries, page numbers should start from 0 (*compatible with Spring Data JPA*).
|
||||
- **Data Access Method Naming Conventions**:
|
||||
- For **querying data lists**: methods **must** start with `selectListBy`, followed by the filter criteria (e.g., `selectListByUserId`, `selectListByDepartmentIdAndStatus`). These methods must also include a `PageRequest` parameter for pagination.
|
||||
- For querying **single data records**: methods **must** start with `selectOne` (e.g., `selectOneById`, `selectOneByUsername`).
|
||||
- For **saving new data**: methods **must** be named `save`, and Mapper method return type must be `int` (affected row count).
|
||||
- For **updating existing data**: methods **must** be named `update`, and Mapper method return type must be `int` (affected row count).
|
||||
- For **deleting data**: methods **must** start with `deleteBy`, clearly indicating the deletion criteria (e.g., `deleteById`). Mapper method return type must be `int` (affected row count).
|
||||
|
||||
- **Controllers**:
|
||||
- Annotate REST controllers with `@RestController`.
|
||||
- Use `@GetMapping`, `@PostMapping`, etc. to map HTTP methods (GET, POST, PUT, DELETE) to appropriate controller methods.
|
||||
- Ensure request and response payloads are clearly defined (DTOs) and documented.
|
||||
- Controllers should primarily handle HTTP request/response mapping and delegate actual business logic to the service layer.
|
||||
|
||||
- **DTOs (Data Transfer Objects)**: Define separate DTOs for request and response bodies to decouple the internal domain model from the API contract. Use validation annotations on DTOs (e.g., `@Valid`, `@NotNull`, `@Size`).
|
||||
|
||||
- **Logging (SLF4J & Logback)**:
|
||||
- All logging in the application uses `org.slf4j.Logger`.
|
||||
- Configure `application-dev.yml` to set logging levels, appenders, and output formats.
|
||||
- Log messages should be descriptive and provide sufficient context. Avoid logging sensitive information.
|
||||
- Use parameterised logging for performance and to prevent string concatenation overhead (e.g., `log.debug("Processing user: {}", userId);`).
|
||||
- Standard log levels: `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
Backend applications follow a structured Gradle project layout. The core application should be clearly divided into various sub-packages.
|
||||
|
||||
```text
|
||||
backend-application
|
||||
├── build.gradle.kts // Project's main Gradle build script
|
||||
├── config // External configuration directory
|
||||
│ ├── application-dev.yml // Application properties for the development environment
|
||||
│ └── application-prod.yml.example // Example production properties (to be copied and configured)
|
||||
├── database // Database-related files
|
||||
│ └── init.d // Database initialisation scripts
|
||||
│ └── init-en_GB.sql // SQL script for database schema and initial data using the specified locale (British English)
|
||||
├── gradle // Gradle Wrapper and configuration files
|
||||
│ ├── libs.versions.toml // Centralised dependency version management (Gradle Version Catalog)
|
||||
│ └── wrapper // Gradle Wrapper files
|
||||
│ ├── gradle-wrapper.jar
|
||||
│ └── gradle-wrapper.properties
|
||||
├── gradle.properties // Project-specific Gradle properties
|
||||
├── gradlew // Gradle Wrapper executable (Linux/macOS)
|
||||
├── gradlew.bat // Gradle Wrapper executable (Windows)
|
||||
├── settings.gradle.kts // Gradle settings for multi-project builds (if applicable)
|
||||
└── src
|
||||
├── main
|
||||
│ ├── java
|
||||
│ │ └── com/onixbyte/application // Application root package
|
||||
│ │ ├── Application.java // Spring Boot application main entry point
|
||||
│ │ ├── config // Spring configuration classes
|
||||
│ │ ├── constant // Classes defining application-wide constants
|
||||
│ │ ├── controller // REST API endpoints
|
||||
│ │ ├── domain // Core domain models and related types
|
||||
│ │ │ ├── common // Common domain objects/utilities
|
||||
│ │ │ ├── entity // JPA/MyBatis entities representing database tables
|
||||
│ │ │ ├── model // General-purpose entity class models
|
||||
│ │ │ ├── view // Data transfer objects specifically for read-only operations (e.g., query results, report structures)
|
||||
│ │ │ └── web // Data transfer objects specifically for web request/response bodies
|
||||
│ │ │ ├── request // Request DTOs
|
||||
│ │ │ └── response // Response DTOs
|
||||
│ │ ├── exception // Custom application-specific exceptions
|
||||
│ │ ├── extension // Extension points or custom functionality
|
||||
│ │ │ ├── jackson // Jackson serialisation/deserialisation extensions
|
||||
│ │ │ └── redis // Redis-related extensions
|
||||
│ │ │ └── serializer
|
||||
│ │ ├── filter // Servlet filters or Spring Security filters
|
||||
│ │ ├── manager // Business logic coordinators, typically orchestrating multiple services or repositories
|
||||
│ │ ├── mapper // Data access layer (MyBatis interfaces)
|
||||
│ │ ├── processor // General-purpose processing components or business workflows
|
||||
│ │ ├── properties // Classes for type-safe configuration properties (`@ConfigurationProperties`)
|
||||
│ │ ├── repository // Data access layer (Spring Data JPA interfaces)
|
||||
│ │ ├── security // Spring Security-specific components
|
||||
│ │ │ ├── authentication // Custom authentication mechanisms
|
||||
│ │ │ └── provider // Custom authentication providers
|
||||
│ │ ├── service // Core business logic (transactional layer)
|
||||
│ │ ├── utils // General-purpose utility classes
|
||||
│ │ └── validation // Custom validation logic
|
||||
│ │ └── group // Validation groups for different contexts (e.g., create, update)
|
||||
│ └── resources
|
||||
│ ├── application.yml // Default application properties
|
||||
│ └── mapper // MyBatis XML mapping files
|
||||
└── test
|
||||
└── java
|
||||
└── com/onixbyte/helix
|
||||
└── HelixApplicationTests.java // Spring Boot integration tests
|
||||
```
|
||||
|
||||
**Key Observations and Specific Instructions:**
|
||||
|
||||
- **External `config` Directory**
|
||||
- Environment configurations (`application-dev.yml`, `application-prod.yml.example`) are managed in the top-level `config` directory, separate from `src/main/resources`. This facilitates environment-specific property management, allowing different configurations to be mounted or linked at deployment time.
|
||||
- **Do not upload any configuration files other than `src/main/resources/application.yml` to the Git repository.**
|
||||
- **Database Initialisation**: The `database/init.d` directory is reserved for SQL scripts, specifically database schema initialisation (`init-en_GB.sql`), which is critical for environment setup and CI/CD pipelines. This structure suggests a "schema-first" or "code-driven schema evolution" approach.
|
||||
- **`client` Package**: This package is used to provide services for all middleware to the application, such as HTTP, S3 storage, Redis calls, etc. For self-coded functional implementations that need to be added to the Spring context (such as JSON Web Token generation and parsing), placing them in this package is also recommended.
|
||||
- **MyBatis & Spring Data JPA Integration**
|
||||
- The `src/main/java/.../mapper` package contains MyBatis mapper interfaces, while the actual SQL definitions reside in `src/main/resources/mapper/*.xml` files. This separation is key to keeping code clean while leveraging MyBatis's powerful XML mapping capabilities.
|
||||
- The `src/main/java/.../repository` package contains Spring Data JPA interfaces.
|
||||
- **`domain` Package Granularity**:
|
||||
- `domain.entity`: Reserved for classes directly mapped to database tables (POJOs for MyBatis).
|
||||
- `domain.model`: For more general-purpose domain objects or aggregate roots that do not map one-to-one with individual tables.
|
||||
- `domain.view`: Specifically for Data Transfer Objects (DTOs) used in read-only scenarios (e.g., query results, report structures).
|
||||
- `domain.web.request` / `domain.web.response`: Clearly separated DTOs for incoming API requests and outgoing API responses, strictly adhering to the API contract and decoupled from internal domain entities.
|
||||
- **`manager` and `processor` Packages**: These packages imply a layered architecture where "managers" coordinate operations involving multiple services or repositories, while "processors" may handle specific aspects of business processes. It is mandatory to clearly define the responsibilities of classes in these packages to prevent anti-patterns such as "anaemic domain model" or "god objects".
|
||||
- **`security` Package**: This sub-package contains custom Spring Security components beyond the initial configuration, such as custom authentication types and providers, indicating a tailored security implementation.
|
||||
- **`properties` Package**: This package is for custom `@ConfigurationProperties` classes, facilitating type-safe access to application settings defined in YML files — well-positioned.
|
||||
- **`extension` Package**: This is a flexible area for application-specific extensions, such as custom Jackson serialisers or Redis customisations. It should be used sparingly to avoid becoming a "miscellaneous" dumping ground.
|
||||
|
||||
## Security (Spring Security)
|
||||
|
||||
- **Mandatory Use**: Spring Security is mandatory in all Spring Boot web applications.
|
||||
- **Authentication & Authorisation**: Configure authentication mechanisms (e.g., OAuth 2.0, JWT, session-based) and authorisation rules in `SecurityConfig.java`.
|
||||
- **CSRF Protection**: Ensure CSRF protection is enabled for state-modifying operations, unless there is a strong reason to disable it (e.g., stateless APIs with other mechanisms already in place).
|
||||
- **CORS**: Correctly configure Cross-Origin Resource Sharing (CORS) according to frontend deployment requirements.
|
||||
- **Third-Party Identity Providers**: When integrating with third-party identity providers (e.g., Microsoft Entra ID), follow best practices for secure token handling and user provisioning. Sensitive credentials must be securely managed (e.g., environment variables, Vault).
|
||||
- **Input Validation**: Always validate all user input on the server side to prevent common vulnerabilities such as SQL injection, XSS, etc.
|
||||
- **Content Security Policy (CSP)**: Consider implementing a robust CSP for the frontend to mitigate XSS attacks.
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Fix macOS Terminal Host Name Showing IP Segments Under Private DNS
|
||||
tags:
|
||||
- macos
|
||||
- dns
|
||||
- terminal
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
In some enterprise or home private network environments, reverse DNS lookups may resolve a device's private IP to a hostname starting with `192`, `172`, or `10`. When this happens, the macOS terminal prompt changes from the normal `user@MacBook-Pro` to something like `user@192-168-1-100`, which can be distracting.
|
||||
|
||||
## Cause
|
||||
|
||||
When starting a terminal session, macOS performs a reverse DNS lookup to determine the hostname for the current IP address. If the private DNS server returns a hostname derived from IP octets (e.g. `192-168-1-100.example.com`), the system adopts it as the Host Name and the terminal prompt reflects it.
|
||||
|
||||
## Fix
|
||||
|
||||
Use the built-in `scutil` (System Configuration Utility) to pin the Host Name to your preferred value.
|
||||
|
||||
### Check Current State
|
||||
|
||||
```shell
|
||||
# View the current Host Name (may be empty or overridden by DNS)
|
||||
scutil --get HostName
|
||||
|
||||
# View the local Bonjour name
|
||||
scutil --get LocalHostName
|
||||
|
||||
# View the computer name shown in Finder
|
||||
scutil --get ComputerName
|
||||
```
|
||||
|
||||
### Set the Host Name
|
||||
|
||||
```shell
|
||||
sudo scutil --set HostName "MacBook-Pro"
|
||||
```
|
||||
|
||||
Prefer a name without spaces or special characters, such as `MacBook-Pro`, `My-Mac`, or your device serial number.
|
||||
|
||||
### Verify
|
||||
|
||||
Open a new terminal window — the value after `@` in the prompt should now show your chosen hostname.
|
||||
|
||||
```shell
|
||||
scutil --get HostName
|
||||
# Output: MacBook-Pro
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `HostName` only affects the network-level hostname. `LocalHostName` (Bonjour) and `ComputerName` (Finder display) are managed independently.
|
||||
- If the issue returns after a reboot, check `/etc/hosts` for conflicting entries or verify whether the DHCP/DNS server continues to push an undesired hostname.
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
title: MinIO Administration Guide for New Versions
|
||||
tags:
|
||||
- minio
|
||||
- storage
|
||||
- s3
|
||||
- devops
|
||||
- cheatsheet
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
In newer versions, MinIO has removed administrative functionality from the Web UI. You now need to use the **MinIO Client (mc)** command-line tool for all management operations.
|
||||
|
||||
## Installing MinIO Client (mc)
|
||||
|
||||
### Windows:
|
||||
|
||||
```powershell
|
||||
# Download mc.exe
|
||||
Invoke-WebRequest -Uri "https://dl.min.io/client/mc/release/windows-amd64/mc.exe" -OutFile "mc.exe"
|
||||
|
||||
# Or using Chocolatey
|
||||
choco install minio-client
|
||||
```
|
||||
|
||||
### Linux/macOS:
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
wget https://dl.min.io/client/mc/release/linux-amd64/mc
|
||||
chmod +x mc
|
||||
sudo mv mc /usr/local/bin/
|
||||
|
||||
# macOS
|
||||
brew install minio/stable/mc
|
||||
```
|
||||
|
||||
## Configuring the MinIO Client
|
||||
|
||||
```bash
|
||||
# Add a MinIO server alias
|
||||
mc alias set myminio http://localhost:9000 minioadmin minioadmin
|
||||
|
||||
# Verify the connection
|
||||
mc admin info myminio
|
||||
```
|
||||
|
||||
## User Management
|
||||
|
||||
### Creating Users
|
||||
|
||||
```bash
|
||||
# Create a new user
|
||||
mc admin user add myminio newuser newpassword
|
||||
|
||||
# List all users
|
||||
mc admin user list myminio
|
||||
```
|
||||
|
||||
### Creating Access Keys and Secret Keys
|
||||
|
||||
```bash
|
||||
# Create a service account for a user (generates AccessKey/SecretKey)
|
||||
mc admin user svcacct add myminio newuser
|
||||
|
||||
# Or specify custom AccessKey and SecretKey
|
||||
mc admin user svcacct add myminio newuser --access-key "MYACCESSKEY123" --secret-key "MYSECRETKEY456"
|
||||
|
||||
# View a user's service accounts
|
||||
mc admin user svcacct list myminio newuser
|
||||
```
|
||||
|
||||
## Permission Management
|
||||
|
||||
### Creating Policies
|
||||
|
||||
```bash
|
||||
# Create a policy file policy.json
|
||||
cat > policy.json << EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::mybucket/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Add the policy
|
||||
mc admin policy add myminio mypolicy policy.json
|
||||
|
||||
# Assign the policy to a user
|
||||
mc admin policy set myminio mypolicy user=newuser
|
||||
```
|
||||
|
||||
## Bucket Management
|
||||
|
||||
```bash
|
||||
# Create a bucket
|
||||
mc mb myminio/mybucket
|
||||
|
||||
# List buckets
|
||||
mc ls myminio
|
||||
|
||||
# Set bucket policy
|
||||
mc policy set public myminio/mybucket
|
||||
```
|
||||
|
||||
## Common Administration Commands
|
||||
|
||||
```bash
|
||||
# View server information
|
||||
mc admin info myminio
|
||||
|
||||
# View server configuration
|
||||
mc admin config get myminio
|
||||
|
||||
# Restart the server
|
||||
mc admin service restart myminio
|
||||
|
||||
# View logs
|
||||
mc admin logs myminio
|
||||
|
||||
# View statistics
|
||||
mc admin prometheus metrics myminio
|
||||
```
|
||||
|
||||
## Practical Script Example
|
||||
|
||||
Create an administration script `setup-minio.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
MINIO_ALIAS="myminio"
|
||||
MINIO_URL="http://localhost:9000"
|
||||
ADMIN_USER="minioadmin"
|
||||
ADMIN_PASS="minioadmin"
|
||||
|
||||
# Configure the MinIO client
|
||||
mc alias set $MINIO_ALIAS $MINIO_URL $ADMIN_USER $ADMIN_PASS
|
||||
|
||||
# Create an application user
|
||||
APP_USER="appuser"
|
||||
APP_PASS="apppassword"
|
||||
mc admin user add $MINIO_ALIAS $APP_USER $APP_PASS
|
||||
|
||||
# Create a service account and retrieve AccessKey/SecretKey
|
||||
echo "Creating service account for $APP_USER..."
|
||||
CREDENTIALS=$(mc admin user svcacct add $MINIO_ALIAS $APP_USER --json)
|
||||
ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.accessKey')
|
||||
SECRET_KEY=$(echo $CREDENTIALS | jq -r '.secretKey')
|
||||
|
||||
echo "Generated credentials:"
|
||||
echo "Access Key: $ACCESS_KEY"
|
||||
echo "Secret Key: $SECRET_KEY"
|
||||
|
||||
# Create a bucket
|
||||
mc mb $MINIO_ALIAS/app-bucket
|
||||
|
||||
# Set a read-only policy
|
||||
mc policy set download $MINIO_ALIAS/app-bucket
|
||||
```
|
||||
|
||||
## Web Console Access
|
||||
|
||||
Although administrative functionality has been removed, you can still access the MinIO Console via:
|
||||
|
||||
```bash
|
||||
# Launch the MinIO Console (if installed separately)
|
||||
mc admin console myminio
|
||||
```
|
||||
|
||||
Alternatively, specify the console address when starting the MinIO server:
|
||||
|
||||
```bash
|
||||
minio server /data --console-address ":9001"
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
Managing the new MinIO relies entirely on the `mc` command-line tool:
|
||||
|
||||
1. **Install the mc client**
|
||||
2. **Configure the server alias**
|
||||
3. **Use `mc admin` commands for user, permission, and bucket management**
|
||||
4. **Generate AccessKeys/SecretKeys via `mc admin user svcacct`**
|
||||
|
||||
While this approach requires command-line operations, it provides more powerful and flexible management capabilities, particularly suited for automated deployment and script-based management.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: MyBatis Flex
|
||||
tags:
|
||||
- java
|
||||
- mybatis
|
||||
- spring-boot
|
||||
- orm
|
||||
- framework
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
> This article only aims at using `MyBatis Flex` in Spring Boot 3
|
||||
|
||||
## Installation and Configuration
|
||||
|
||||
### Installation
|
||||
|
||||
Add the following codes to `libs.versions.toml` in `gradle` :
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
mybatisFlexVersion = "X.Y.Z"
|
||||
hikariVersion = "X.Y.Z"
|
||||
|
||||
[libraries]
|
||||
hikari = { group = "com.zaxxer", name = "HikariCP", version.ref = "hikariVersion" }
|
||||
mybatisFlex-processor = { group = "com.mybatis-flex", name = "mybatis-flex-processor", version.ref = "mybatisFlexVersion" }
|
||||
mybatisFlex-starter = { group = "com.mybatis-flex", name = "mybatis-flex-spring-boot3-starter", version.ref = "mybatisFlexVersion" }
|
||||
```
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: Accelerating Fuzzy Search in PostgreSQL with Tokenisation
|
||||
tags:
|
||||
- postgresql
|
||||
- zhparser
|
||||
- full-text-search
|
||||
- performance
|
||||
author:
|
||||
name: Siu Jam Oh
|
||||
email: jamo.siu@gmail.com
|
||||
---
|
||||
|
||||
## Background and Challenges
|
||||
|
||||
As our business data surpassed **2 million rows**, traditional `LIKE '%keyword%'` fuzzy queries triggered frequent database I/O alerts, with query response times degrading from milliseconds to seconds. To improve search efficiency and support Chinese semantics, we decided to introduce the `zhparser` extension for full-text search.
|
||||
|
||||
## Evolution Path and Environment Adaptation
|
||||
|
||||
This implementation went through four key phases, each addressing distinct technical challenges:
|
||||
|
||||
### CentOS 7.9 VM (Feasibility Validation)
|
||||
|
||||
- **Goal**: Validate the compatibility of `SCWS` + `zhparser` on older systems.
|
||||
- **Core Action**: Manually compiled `postgresql-16.2` from source in a CentOS 7.9 environment, got the extension working end-to-end.
|
||||
|
||||
**Conclusion**: Confirmed significant performance improvements from the tokenisation approach for Chinese search.
|
||||
|
||||
### Local Docker Container (Containerisation Exploration)
|
||||
|
||||
- **Goal**: Initial testing in a complete local system.
|
||||
- **Core Action**: Injected binary `.so` files via `docker cp`, resolved `ldconfig` dynamic library path visibility issues.
|
||||
- **Discovery**: Identified that **missing dictionary files** cause tokenisation to degrade into single-character (particle-level) tokenisation — a critical failure point.
|
||||
|
||||
### EulerOS 2.0 Test Server (Self-Compiled Environment Adaptation)
|
||||
|
||||
- **Goal**: Adapt to the production OS architecture (x86_64) and self-compiled PostgreSQL installation.
|
||||
- **Core Issue**: Resolved `libscws.so.1` loading errors.
|
||||
- **Key Solutions**:
|
||||
- Ensured the `postgres` runtime user has access permissions to `/usr/local/scws/lib`.
|
||||
- Modified `systemd` service environment variables or created `/usr/lib64` symlinks to force refresh library search paths.
|
||||
|
||||
### Production Deployment Preparation (Final Tuning)
|
||||
|
||||
- **Goal**: Ensure query stability at 2M+ data volume.
|
||||
- **Optimisation**: Addressed cases where non-semantic fragments (e.g., "古唐合") returned no results by establishing a "full-text search first + `pg_trgm` index assist" degraded query strategy.
|
||||
|
||||
## Core Installation and Configuration Steps (Self-Compiled Environments)
|
||||
|
||||
### Installing the `SCWS` Tokenisation Engine
|
||||
|
||||
SCWS is the underlying core dependency of `zhparser` and must be installed first.
|
||||
|
||||
1. **Download and Extract**: Download the source package (e.g., `scws-1.2.3`).
|
||||
2. **Compile and Install**:
|
||||
|
||||
```bash
|
||||
./configure --prefix=/usr/local/scws
|
||||
make && make install
|
||||
```
|
||||
|
||||
3. **Verify the Library**: Ensure `/usr/local/scws/lib/libscws.so.1` exists.
|
||||
|
||||
### Compiling and Installing `zhparser`
|
||||
|
||||
This step requires `pg_config` from the self-compiled PostgreSQL installation.
|
||||
|
||||
1. **Get the Source**: Clone the `zhparser` project from GitHub.
|
||||
2. **Compile with Specified Path**:
|
||||
|
||||
```bash
|
||||
# Ensure pg_config is in PATH, or specify manually
|
||||
make USE_PGXS=1 PG_CONFIG=/usr/local/pgsql/bin/pg_config
|
||||
make USE_PGXS=1 PG_CONFIG=/usr/local/pgsql/bin/pg_config install
|
||||
```
|
||||
|
||||
*Note: The `install` step automatically places `zhparser.so` into PG's `pkglibdir` and extension scripts into the `extension` directory.*
|
||||
|
||||
### Resolving Dynamic Library Dependencies
|
||||
|
||||
1. **Refresh System Cache**:
|
||||
|
||||
```bash
|
||||
echo "/usr/local/scws/lib" > /etc/ld.so.conf.d/scws.conf
|
||||
ldconfig
|
||||
```
|
||||
|
||||
2. **Permission Check**: Ensure the OS user running `postgres` has `rx` permission on `/usr/local/scws/lib`.
|
||||
3. **Force Symlink (Alternative)**: If `ldconfig` fails, symlink the library file to `/usr/lib64`.
|
||||
|
||||
### Restarting the Database
|
||||
|
||||
After modifying system shared library configuration, the PostgreSQL process must be restarted to reload environment variables and linked libraries.
|
||||
|
||||
```bash
|
||||
## Restart using pg_ctl (paths may vary for self-compiled installations)
|
||||
/usr/local/pgsql/bin/pg_ctl -D /usr/local/pgsql/data restart
|
||||
|
||||
## Or restart via systemd (if registered as a service)
|
||||
systemctl restart postgresql
|
||||
```
|
||||
|
||||
### Database-Level Initialisation
|
||||
|
||||
Connect to `psql` and run the logical configuration:
|
||||
|
||||
```sql
|
||||
-- Create the extension
|
||||
CREATE EXTENSION zhparser;
|
||||
|
||||
-- Create a full-text search configuration and bind the tokeniser
|
||||
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);
|
||||
|
||||
-- Add token mappings
|
||||
ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l,t,b WITH simple;
|
||||
|
||||
-- [Optional] Specify the dictionary path for self-compiled installations
|
||||
-- ALTER DATABASE postgres SET zhparser.dict_path = '/usr/local/scws/etc/dict.utf8.xdb';
|
||||
```
|
||||
|
||||
## Performance Analysis and Pitfalls
|
||||
|
||||
### Index Performance Bottleneck Analysis
|
||||
|
||||
During testing, it was found that even exact queries suffered from `Bitmap Index Scan` due to an improperly designed composite index (with `create_time` as the leading column), resulting in query times as high as **482 ms**.
|
||||
|
||||
- **Improvement**: Created single-column **B-tree** indexes on frequently searched columns, reducing response time to under **10 ms** with `Index Scan`.
|
||||
|
||||
### GIN Index and Non-Semantic Matching
|
||||
|
||||
- **Cross-Word Truncation**: The tokenisation engine is semantic-based, so truncated strings like "古唐合" may fail to match with `@@` due to tokenisation boundaries.
|
||||
- **Mitigation Strategy**: Adopt a "waterfall search" approach. Full-text search (FTS) first; if the result set is empty, automatically degrade to `LIKE` fuzzy queries, assisted by `pg_trgm` indexing.
|
||||
|
||||
## Final Deployment Strategy: Dual-Track Parallel Retrieval
|
||||
|
||||
After analysing the data, we found that approximately 1.24% of account names contain non-standard Simplified Chinese characters, and some company names in the database are unusual enough to cause search failures. We adopted a "stepwise degradation" strategy:
|
||||
|
||||
- **Step 1: Full-Text Search (Fast Track)**: Use GIN index for `@@` matching.
|
||||
- **Step 2: Result Evaluation**: If the result set is empty, check whether the search term contains letters or suspected Traditional Chinese characters.
|
||||
- **Step 3: Fuzzy Fallback (Safe Fallback)**: Execute `LIKE '%keyword%'`. Although slower, since this serves as a "gap-fill" logic triggered only ~1% of the time, it won't impose overall system pressure.
|
||||
|
||||
## Search Optimisation: Integrating a Custom Business Lexicon
|
||||
|
||||
To address issues like company brand names being incorrectly segmented by full-text search (e.g., "元一" being split into a numeral and a quantifier), we built an automated maintenance pipeline from data extraction to index rebuild.
|
||||
|
||||
### Lexicon Extraction and Preprocessing
|
||||
|
||||
Leverage the structural parsing capabilities of **`companynameparser`** to strip region names and industry suffixes, and use **`jieba`** for semantic validation to ensure core brand name integrity:
|
||||
|
||||
- **Extraction Logic**: Traverse all `buyer_unique_name` values via a Python script, extracting the core `brand` field.
|
||||
- **Weight Compensation**: For words prone to fragmentation (e.g., those containing "元", "一", "三"), manually boost TF (term frequency weight) to **50.0–60.0** to ensure their priority overrides built-in quantifier rules.
|
||||
- **Output Specification**: Produce SCWS-compliant 4-field `UTF-8` text (WORD, TF, IDF, ATTR). Use tab `\t` separators to avoid parsing anomalies.
|
||||
|
||||
### Lexicon Compilation and Deployment
|
||||
|
||||
:::tip
|
||||
Users compiling `xdb` binary dictionary files on Windows can visit OnixByte’s [GitHub](https://github.com/onixbyte/scws/releases/tag/1.2.3) or [GitLab](https://git.onixbyte.cn/onixbyte/scws/-/releases/1.2.3) pages to download the native scws command-line tool for Windows, pre-compiled using MingW.
|
||||
:::
|
||||
|
||||
Convert the text dictionary to SCWS's efficient binary format (XDB):
|
||||
|
||||
1. **Compile the Binary Dictionary**:
|
||||
|
||||
```bash
|
||||
# Use scws-gen-dict to generate an encrypted binary lexicon
|
||||
/usr/local/scws/bin/scws-gen-dict -i custom_company.txt -o /usr/local/scws/etc/custom_company.xdb -c utf8
|
||||
```
|
||||
|
||||
2. **File Distribution and Permissions**: Move the generated `.xdb` file to the tokenisation data directory and ensure the `postgres` user has read permission:
|
||||
|
||||
```bash
|
||||
cp custom_company.xdb /usr/local/pgsql/share/tsearch_data/
|
||||
chown postgres:postgres /usr/local/pgsql/share/tsearch_data/custom_company.xdb
|
||||
```
|
||||
|
||||
### Database Parameter Configuration
|
||||
|
||||
Modify `postgresql.conf` to force-load `zhparser` and its custom extension lexicon:
|
||||
|
||||
```plain text
|
||||
## Preload the extension library (requires restart to take effect)
|
||||
shared_preload_libraries = 'zhparser'
|
||||
|
||||
## Load custom external dictionaries (use paths relative to tsearch_data)
|
||||
zhparser.extra_dicts = 'custom_company.xdb'
|
||||
```
|
||||
|
||||
### Hot Index Rebuild and Verification
|
||||
|
||||
Since tokenisation rules have changed, existing data must be semantically synchronised via index rebuild:
|
||||
|
||||
**Physically Restart the Service**:
|
||||
|
||||
```bash
|
||||
su - postgres -c "/usr/local/pgsql/bin/pg_ctl -D /usr/local/pgsql/data restart"
|
||||
```
|
||||
|
||||
**Online Index Rebuild**: Use the `CONCURRENTLY` keyword to refresh the GIN index without blocking DML operations on 400K rows:
|
||||
|
||||
```bash
|
||||
REINDEX INDEX CONCURRENTLY index_name;
|
||||
```
|
||||
|
||||
**Tokenisation Effectiveness Verification**:
|
||||
|
||||
```sql
|
||||
-- Expected part-of-speech should show as n (noun), not x (unknown)
|
||||
SELECT * FROM ts_debug('chinese', '元一能源');
|
||||
```
|
||||
|
||||
**Optimisation Notes:**
|
||||
- **Explicit Weight Compensation**: This is the key technique that resolved the "元一" tokenisation failure (shown as `x`).
|
||||
- **Distinguish Restart from Reload**: `shared_preload_libraries` must be activated via `restart`, not a simple reload.
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: The Quartile Method
|
||||
tags:
|
||||
- statistics
|
||||
- algorithm
|
||||
- data-analysis
|
||||
- math
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
The quartile method is a commonly used statistical technique primarily employed for data analysis and presentation. The method divides a data set into four equal parts, each containing one quarter of the data. The key statistics include the first quartile (Q1), second quartile (Q2, the median), and third quartile (Q3).
|
||||
|
||||
- The first quartile (`Q1`), also known as the lower quartile, is the value at the 25th percentile of a data set sorted in ascending order.
|
||||
- The second quartile (`Q2`), also known as the median, is the value at the 50th percentile of a data set sorted in ascending order.
|
||||
- The third quartile (`Q3`), also known as the upper quartile, is the value at the 75th percentile of a data set sorted in ascending order.
|
||||
- The interquartile range (`IQR`) is the difference between the third quartile and the first quartile, used to measure the dispersion of the middle 50% of data. The formula is IQR = Q3 - Q1. The IQR is commonly used in constructing box plots, an effective way to describe data distribution, particularly useful for identifying outliers.
|
||||
|
||||
Upper bound = Q3 + 1.5 × IQR.
|
||||
|
||||
Lower bound = Q1 - 1.5 × IQR.
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Setting Up an LDAP Service
|
||||
tags:
|
||||
- ldap
|
||||
- openldap
|
||||
- authentication
|
||||
- linux
|
||||
- devops
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
Setting up an LDAP (Lightweight Directory Access Protocol) server is a core step in implementing enterprise-level **centralised identity authentication** and **permission management**. The most commonly used open-source implementation is **OpenLDAP**.
|
||||
|
||||
Below are detailed steps for setting up an OpenLDAP server on Debian/Ubuntu-based Linux systems.
|
||||
|
||||
## Environment Preparation and Installation
|
||||
|
||||
Before starting, ensure your system packages are up to date and the hostname is properly configured.
|
||||
|
||||
```bash
|
||||
## Update the system
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
## Install OpenLDAP and management tools
|
||||
## slapd is the daemon, ldap-utils is the command-line client tool
|
||||
sudo apt install slapd ldap-utils -y
|
||||
```
|
||||
|
||||
> During installation, you will be prompted to set the **LDAP administrator password**. Be sure to remember this password — it will be used frequently during configuration.
|
||||
|
||||
## Configuring the OpenLDAP Server
|
||||
|
||||
Although the installation performs some initial setup, we typically need to customise the configuration for a specific domain (e.g., `example.com`).
|
||||
|
||||
### Reconfigure slapd
|
||||
|
||||
Run the following command to enter the interactive configuration interface:
|
||||
|
||||
```bash
|
||||
sudo dpkg-reconfigure slapd
|
||||
```
|
||||
|
||||
**Configuration recommendations**:
|
||||
|
||||
1. **Omit OpenLDAP server configuration?** Choose **No**.
|
||||
2. **DNS domain name:** Enter your domain (e.g., `centre.example.com`). This determines your Base DN, such as `dc=centre,dc=example,dc=com`.
|
||||
3. **Organization name:** Enter your organisation name.
|
||||
4. **Administrator password:** Enter the administrator password you set earlier.
|
||||
5. **Database backend:** **MDB** is recommended.
|
||||
6. **Remove database when slapd is purged?** Choose **No**.
|
||||
7. **Move old database?** Choose **Yes**.
|
||||
|
||||
## Understanding the LDAP Hierarchy
|
||||
|
||||
LDAP data is stored in a **tree structure**. To facilitate management, we typically create two "Organisational Units" (OUs): one for users (People) and one for groups (Groups).
|
||||
|
||||
## Creating Organisational Units (OUs)
|
||||
|
||||
In LDAP, we use **LDIF** (LDAP Data Interchange Format) files to add or modify directory entries.
|
||||
|
||||
Create a file named `base.ldif`:
|
||||
|
||||
```text
|
||||
## Create the users organisational unit
|
||||
dn: ou=people,dc=centre,dc=example,dc=com
|
||||
objectClass: organizationalUnit
|
||||
ou: people
|
||||
|
||||
## Create the groups organisational unit
|
||||
dn: ou=groups,dc=centre,dc=example,dc=com
|
||||
objectClass: organizationalUnit
|
||||
ou: groups
|
||||
```
|
||||
|
||||
**Import the data:**
|
||||
|
||||
```bash
|
||||
## Import the LDIF file into the database as the administrator
|
||||
ldapadd -x -D "cn=admin,dc=centre,dc=example,dc=com" -W -f base.ldif
|
||||
```
|
||||
|
||||
## Adding Users and Groups
|
||||
|
||||
Create a file named `groups.ldif` to define a new group:
|
||||
|
||||
```text
|
||||
## Define a new group
|
||||
dn: cn=developers,ou=groups,dc=centre,dc=example,dc=com
|
||||
objectClass: inetOrgGroup
|
||||
cn: developers
|
||||
```
|
||||
|
||||
Create a file named `users.ldif` to define a new user:
|
||||
|
||||
```text
|
||||
## Define a new user
|
||||
dn: uid=jbloggs,ou=people,dc=centre,dc=example,dc=com # The Distinguished Name (DN) of this entry.
|
||||
objectClass: inetOrgPerson # Object class — inetOrgPerson represents an internet organisation person. It enables common contact attributes such as email, displayName, telephoneNumber, etc.
|
||||
objectClass: posixAccount # Makes this entry compatible with Unix/Linux system accounts. With it, the user can log into Linux servers and has a UID, GID, and home directory.
|
||||
objectClass: shadowAccount # Used to manage password ageing (expiry, change warnings, etc.), corresponding to /etc/shadow functionality in Linux.
|
||||
uid: jbloggs # The user's login name (User ID). This is typically what you enter on the Linux login screen.
|
||||
sn: Bloggs # Surname. Required by the inetOrgPerson class.
|
||||
givenName: Joe # First name.
|
||||
cn: Joe Bloggs # Common Name. The standard display name for an LDAP entry.
|
||||
displayName: Joe Bloggs # The friendly name displayed in graphical interfaces or email clients.
|
||||
uidNumber: 10000 # The user's numeric ID in the Linux system.
|
||||
gidNumber: 5000 # The numeric ID of the user's primary group.
|
||||
userPassword: {SSHA}password-hash-or-plaintext # The user's encrypted password.
|
||||
homeDirectory: /home/jbloggs # The path to the user's home directory after logging into Linux.
|
||||
loginShell: /bin/bash # The shell environment the user gets after logging in.
|
||||
```
|
||||
|
||||
> If you plan to integrate **GitLab**, **Jenkins**, or a **VPN** later, they typically search the `uid` attribute to verify login names.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### If the user doesn't need server login permissions, can the `objectClass: posixAccount` be removed?
|
||||
|
||||
**Yes, it can be completely removed**, but you need to be aware of the "binding relationships" between attributes.
|
||||
|
||||
If you only need this user for web application logins (e.g., GitLab, Jenkins, Wiki) or as an email contact, and they don't need to log into a Linux server via SSH or console, removing `posixAccount` is the more standard approach.
|
||||
|
||||
When you remove `objectClass: posixAccount`, the following attributes **must also be deleted**, as they belong to that class's mandatory or optional attributes:
|
||||
|
||||
- `uidNumber`
|
||||
- `gidNumber`
|
||||
- `homeDirectory`
|
||||
- `loginShell`
|
||||
|
||||
Additionally, `shadowAccount` is also typically associated with system logins — if you don't need to manage Linux password expiry policies, it can also be removed.
|
||||
|
||||
## Follow-up Suggestions and Management Tools
|
||||
|
||||
Command-line LDAP management can be cumbersome. The following tools are recommended for visual administration:
|
||||
|
||||
- **phpLDAPAdmin**: A web-based management interface, ideal for quick onboarding.
|
||||
- **Apache Directory Studio**: A powerful cross-platform desktop client, suitable for complex architecture design.
|
||||
- **Security Hardening**: By default, LDAP transmits data in plain text. It is recommended to configure **LDAPS (LDAP over SSL/TLS)** to encrypt communication on port 636.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Using JSON in MySQL
|
||||
tags:
|
||||
- mysql
|
||||
- json
|
||||
- database
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
MySQL (since version 5.7) **does not directly support a data type called `jsonb`**. `jsonb` is a data type specific to PostgreSQL, which stores JSON data in a binary format with pre-parsing for faster access and manipulation during queries.
|
||||
|
||||
However, MySQL's `JSON` data type is functionally and internally similar to PostgreSQL's `jsonb` in many respects, particularly when it comes to querying data.
|
||||
|
||||
The `JSON` data type in MySQL was introduced in MySQL 5.7 and has the following characteristics:
|
||||
|
||||
1. **Binary Storage**: Like PostgreSQL's `jsonb`, MySQL's `JSON` type data is stored in an **internal binary format** rather than as a plain text string. This makes reading and manipulating JSON data more efficient, as the database does not need to parse text-format JSON strings on every query.
|
||||
2. **Automatic Validation**: When you insert or update a `JSON` column, MySQL automatically validates that its content is a valid JSON document. If not, it throws an error.
|
||||
3. **Optimised Storage**: The binary format is also space-optimised, typically more compact than storing JSON in raw text format.
|
||||
|
||||
MySQL provides a powerful set of functions and operators for querying and manipulating `JSON` data, very similar to what you'd expect from `jsonb`:
|
||||
|
||||
1. **`->` (JSON Extract Operator)**: Extracts a value from a JSON document. It returns a JSON value.
|
||||
|
||||
```sql
|
||||
SELECT my_json_column->'$.key' FROM my_table;
|
||||
-- Example: extract the name property of a user object
|
||||
-- Assuming my_json_column stores {'user': {'name': 'Alice'}}
|
||||
SELECT json_data->'$.user.name' FROM my_table;
|
||||
```
|
||||
|
||||
2. **`->>` (JSON Unquote Operator)**: Extracts a value from a JSON document and **automatically unquotes it**, typically returning a scalar value (e.g., string, number). This is equivalent to `JSON_UNQUOTE(JSON_EXTRACT(...))`.
|
||||
|
||||
```sql
|
||||
SELECT my_json_column->>'$.key' FROM my_table;
|
||||
-- Example: extract the name property of a user object (returns the string 'Alice' directly)
|
||||
SELECT json_data->>'$.user.name' FROM my_table;
|
||||
```
|
||||
|
||||
3. **`JSON_EXTRACT(json_doc, path, ...)`**: Explicitly extracts data from a JSON document.
|
||||
|
||||
```sql
|
||||
SELECT JSON_EXTRACT(my_json_column, '$.key') FROM my_table;
|
||||
```
|
||||
|
||||
4. **`JSON_CONTAINS(json_doc, candidate, path)`**: Checks whether a JSON document contains a specified value.
|
||||
|
||||
```sql
|
||||
-- Check whether the tags array contains 'backend'
|
||||
-- Assuming my_json_column stores {'tags': ['frontend', 'backend']}
|
||||
SELECT * FROM my_table WHERE JSON_CONTAINS(json_data->'$.tags', '"backend"');
|
||||
```
|
||||
|
||||
5. **`JSON_SEARCH(json_doc, one_or_all, search_str, escape_char, path, ...)`**: Returns the path to a specified string within a JSON document.
|
||||
|
||||
```sql
|
||||
-- Find the path to a value of 'test'
|
||||
SELECT JSON_SEARCH(my_json_column, 'one', 'test') FROM my_table;
|
||||
```
|
||||
|
||||
6. **`JSON_TABLE(json_doc, path COLUMNS ... )` (MySQL 8.0 and later)**: A very powerful function that "expands" JSON data into relational rows and columns, ideal for complex queries and reporting.
|
||||
|
||||
```sql
|
||||
-- Assuming json_data stores {'items': [{'id': 1, 'name': 'A'}, {'id': 2, 'name': 'B'}]}
|
||||
SELECT *
|
||||
FROM my_table,
|
||||
JSON_TABLE(json_data, '$.items[*]' COLUMNS(
|
||||
itemId INT PATH '$.id',
|
||||
itemName VARCHAR(50) PATH '$.name'
|
||||
)) AS jt;
|
||||
```
|
||||
|
||||
Like PostgreSQL's `jsonb`, efficient querying on JSON fields typically requires indexing. Since the content of JSON fields is dynamic, MySQL does not directly support creating traditional B-tree indexes on a specific internal path of a JSON field. However, you can achieve this through **Virtual Generated Columns**:
|
||||
|
||||
1. **Create a Virtual Column**: Define a virtual column whose value is extracted from a specific path in the JSON field.
|
||||
|
||||
```sql
|
||||
ALTER TABLE my_table
|
||||
ADD COLUMN user_name VARCHAR(255) AS (json_data->>'$.user.name') VIRTUAL;
|
||||
```
|
||||
|
||||
2. **Create an Index on the Virtual Column**: This way, when you query `WHERE json_data->>'$.user.name' = 'Alice'`, the MySQL optimiser can use the `idx_user_name` index, significantly improving query performance.
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_user_name ON my_table (user_name);
|
||||
```
|
||||
|
||||
Although MySQL does not have the exact name `jsonb`, its `JSON` data type provides highly similar functionality: binary storage optimisation, automatic validation, and rich query operators and functions. By combining virtual columns with indexes, MySQL can deliver query performance and flexibility comparable to PostgreSQL's `jsonb` when working with JSON data.
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: Use Gravatar in First-party Systems
|
||||
tags:
|
||||
- gravatar
|
||||
- avatar
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
[Gravatar](https://gravatar.com) (Globally Recognised Avatar) is a service that associates avatar images with email
|
||||
addresses. When a user registers on your platform with their email, you can display their Gravatar as a default avatar
|
||||
without building your own image upload and storage pipeline.
|
||||
|
||||
## How It Works
|
||||
|
||||
Gravatar exposes a simple HTTP endpoint. You compute the SHA256 hash of the user's **lowercased and trimmed** email
|
||||
address, then embed it in an image URL:
|
||||
|
||||
```
|
||||
https://gravatar.com/avatar/<hash>
|
||||
```
|
||||
|
||||
## Generating the Hash
|
||||
|
||||
### Node.js
|
||||
|
||||
```js
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
const email = "user@example.com".trim().toLowerCase();
|
||||
const hash = createHash("sha256").update(email).digest("hex");
|
||||
const url = `https://gravatar.com/avatar/${hash}`;
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
|
||||
email = "user@example.com".strip().lower()
|
||||
hash = hashlib.sha256(email.encode()).hexdigest()
|
||||
url = f"https://gravatar.com/avatar/{hash}"
|
||||
```
|
||||
|
||||
### Shell
|
||||
|
||||
```shell
|
||||
echo -n "user@example.com" | tr '[:upper:]' '[:lower:]' | sha256sum | cut -d ' ' -f1
|
||||
```
|
||||
|
||||
## URL Parameters
|
||||
|
||||
The `/avatar/` endpoint accepts several query parameters to customise the result:
|
||||
|
||||
| Parameter | Description | Example |
|
||||
|-----------|-----------------------------------------|----------------|
|
||||
| `s` | Size in pixels (default: 80) | `?s=200` |
|
||||
| `d` | Default image when no Gravatar is found | `?d=identicon` |
|
||||
| `r` | Content rating (`g`, `pg`, `r`, `x`) | `?r=g` |
|
||||
|
||||
### Default Image Options (`d`)
|
||||
|
||||
- `identicon` — a geometric pattern based on the hash
|
||||
- `robohash` — a generated robot image
|
||||
- `retro` — an 8-bit style pixelated face
|
||||
- `monsterid` — a generated monster cartoon
|
||||
- `wavatar` — a generated face
|
||||
- `mp` — a generic silhouette (Mystery Person)
|
||||
- `blank` — a transparent PNG
|
||||
- A custom URL (must be URL-encoded)
|
||||
|
||||
### Putting It Together
|
||||
|
||||
```html
|
||||
<img
|
||||
src="https://gravatar.com/avatar/a3b4c5d6e7f8?s=160&d=robohash&r=g"
|
||||
alt="User avatar"
|
||||
width="160"
|
||||
height="160"
|
||||
/>
|
||||
```
|
||||
|
||||
Always pass the `d` parameter to avoid broken images for users who have not set up a Gravatar. `identicon` and
|
||||
`robohash` are popular choices because they generate a unique, recognisable image for every hash.
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Version Control and Code Review
|
||||
tags:
|
||||
- git
|
||||
- code-review
|
||||
- best-practice
|
||||
- workflow
|
||||
author:
|
||||
name: Zihlu Wang
|
||||
email: real@zihluwang.me
|
||||
---
|
||||
|
||||
## GitFlow Workflow
|
||||
|
||||
Version control will use the GitFlow branching model, consisting of `main`, `develop`, `feature`, `release`, and
|
||||
`hotfix` branches.
|
||||
|
||||
- `main`: Production-ready code. Only `release` and `hotfix` branches are merged into `main`.
|
||||
- `develop`: Integration branch for upcoming features.
|
||||
- `feature/*`: Branches for new features, branched off `develop`.
|
||||
- `release/*`: Branches for preparing new production releases, branched off `develop`.
|
||||
- `hotfix/*`: Branches for urgent production bug fixes, branched off `main`.
|
||||
|
||||
## Pull Requests/Merge Requests
|
||||
|
||||
All code changes (except direct pushes to feature branches) must be submitted via pull requests.
|
||||
|
||||
## Code Review
|
||||
|
||||
- Each pull request must be reviewed by at least one other developer.
|
||||
- Reviewers are responsible for checking compliance with these coding standards, code quality, logical correctness, and
|
||||
test coverage.
|
||||
- IntelliJ IDEA's integrated code analysis tools should be run locally before creating a PR.
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Write clear, concise, and descriptive commit messages that explain what was changed and why. If possible, follow the
|
||||
Conventional Commits format (e.g., `feat: add user registration endpoint`).
|
||||
Reference in New Issue
Block a user