docs: align stack with java 8 and native miniapp

This commit is contained in:
王鹏
2026-07-07 14:56:26 +08:00
parent e853b8f7d8
commit 95685efa82
2 changed files with 533 additions and 124 deletions

View File

@@ -6,7 +6,7 @@
**Architecture:** Use a backend-first implementation: define schema, status machines, API contracts, and tests before building the three clients. Keep the first release as a modular monolith with tenant/community fields on core tables, so later multi-community SaaS work can expand without rewriting order, delivery, and permission models.
**Tech Stack:** Spring Boot 3, Java 17, MyBatis Plus, Sa-Token, MySQL 8, Redis, Flyway, Knife4j/OpenAPI, MinIO-compatible object storage, Vue 3, TypeScript, Vite, Element Plus, uni-app/Vue 3 for WeChat miniapp, Vue 3 mobile H5 for riders.
**Tech Stack:** Spring Boot 2.7.18, Java 8, MyBatis Plus, Sa-Token, MySQL 8, Redis, Flyway, Knife4j/OpenAPI, MinIO-compatible object storage, Vue 3, TypeScript, Vite, Element Plus for admin web, WeChat native mini program with WXML/WXSS/JavaScript, Vue 3 mobile H5 for riders.
---
@@ -62,7 +62,15 @@ D:\codes\LinHelp
│ └── src
├── miniapp
│ ├── package.json
── src
── project.config.json
│ └── miniprogram
│ ├── app.js
│ ├── app.json
│ ├── app.wxss
│ ├── api
│ ├── components
│ ├── pages
│ └── utils
├── rider-h5
│ ├── package.json
│ ├── vite.config.ts
@@ -79,13 +87,13 @@ Boundary rules:
- `backend/src/main/java/com/linhelp/common` contains cross-cutting code only.
- Each business package owns its entity, mapper, service, controller, DTOs, and tests.
- Frontend clients use typed API modules under `src/api`, and pages call APIs through those modules instead of inline `fetch` calls.
- Admin web and rider H5 use API modules under `src/api`; the native mini program uses request modules under `miniprogram/api` and a shared `miniprogram/utils/request.js` wrapper instead of inline `wx.request` calls.
- All tables that carry tenant-owned business data include `tenant_id` and `community_id`.
- Use table names `app_user`, `goods_order`, and `sys_role` instead of reserved or ambiguous names.
## Shared Domain Vocabulary
Use these enum codes in backend and frontend:
Use these enum codes in backend, admin web, rider H5, and miniapp constants:
```text
GoodsOrderStatus: PENDING_CONFIRM, PREPARING, PENDING_DELIVERY, PENDING_PICKUP, DELIVERING, COMPLETED, CANCELED
@@ -110,6 +118,11 @@ DeliveryMethod: IMMEDIATE, SCHEDULED, SELF_PICKUP
- Create: `admin-web/package.json`
- Create: `admin-web/vite.config.ts`
- Create: `miniapp/package.json`
- Create: `miniapp/project.config.json`
- Create: `miniapp/miniprogram/app.js`
- Create: `miniapp/miniprogram/app.json`
- Create: `miniapp/miniprogram/app.wxss`
- Create: `miniapp/miniprogram/utils/request.js`
- Create: `rider-h5/package.json`
- Create: `rider-h5/vite.config.ts`
- Create: `deploy/docker-compose.dev.yml`
@@ -118,7 +131,7 @@ DeliveryMethod: IMMEDIATE, SCHEDULED, SELF_PICKUP
- [ ] **Step 1: Create backend Spring Boot skeleton**
Add `backend/pom.xml` with Java 17, Spring Boot 3, MyBatis Plus, Sa-Token, MySQL, Redis, Flyway, Knife4j, MinIO, validation, and test dependencies.
Add `backend/pom.xml` with Java 8, Spring Boot 2.7.18, MyBatis Plus, Sa-Token, MySQL, Redis, Flyway, Knife4j, MinIO, validation, and test dependencies. Spring Boot 3 is not used because it requires Java 17+.
```xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
@@ -130,11 +143,14 @@ Add `backend/pom.xml` with Java 17, Spring Boot 3, MyBatis Plus, Sa-Token, MySQL
<version>0.1.0-SNAPSHOT</version>
<name>linhelp-backend</name>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.3.2</spring-boot.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>2.7.18</spring-boot.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<sa-token.version>1.38.0</sa-token.version>
<knife4j.version>4.5.0</knife4j.version>
<knife4j.version>4.4.0</knife4j.version>
<minio.version>8.5.11</minio.version>
</properties>
<dependencyManagement>
@@ -163,30 +179,26 @@ Add `backend/pom.xml` with Java 17, Spring Boot 3, MyBatis Plus, Sa-Token, MySQL
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
@@ -202,6 +214,15 @@ Add `backend/pom.xml` with Java 17, Spring Boot 3, MyBatis Plus, Sa-Token, MySQL
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
@@ -290,10 +311,9 @@ spring:
url: jdbc:mysql://localhost:3306/linhelp?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: linhelp
password: linhelp
data:
redis:
host: localhost
port: 6379
redis:
host: localhost
port: 6379
flyway:
enabled: true
locations: classpath:db/migration
@@ -345,37 +365,149 @@ Create `admin-web/package.json`.
}
```
Create `miniapp/package.json`.
Create `miniapp/package.json` for native mini program helper tests and optional WeChat CI commands. Do not add uni-app, Vue, or Vite dependencies to the miniapp.
```json
{
"name": "linhelp-miniapp",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev:mp-weixin": "uni -p mp-weixin",
"build": "uni build -p mp-weixin",
"test": "vitest run"
},
"dependencies": {
"@dcloudio/uni-app": "^3.0.0",
"@dcloudio/uni-mp-weixin": "^3.0.0",
"axios": "^1.7.2",
"pinia": "^2.1.7",
"vue": "^3.4.31"
"test": "jest --runInBand",
"ci:preview": "miniprogram-ci preview --pp ./project.config.json --robot 1"
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-cli-shared": "^3.0.0",
"@dcloudio/vite-plugin-uni": "^3.0.0",
"typescript": "^5.5.3",
"vite": "^5.3.3",
"vitest": "^2.0.2"
"jest": "^29.7.0",
"miniprogram-ci": "^2.0.0"
}
}
```
Create `miniapp/project.config.json`.
```json
{
"appid": "touristappid",
"projectname": "linhelp-miniapp",
"miniprogramRoot": "miniprogram/",
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"minified": true
},
"compileType": "miniprogram"
}
```
Create `miniapp/miniprogram/app.js`.
```js
App({
globalData: {
apiBaseUrl: 'http://localhost:8080',
token: ''
}
})
```
Create `miniapp/miniprogram/app.json`.
```json
{
"pages": [
"pages/home/index",
"pages/products/index",
"pages/products/detail",
"pages/orders/index",
"pages/orders/detail",
"pages/express/create",
"pages/group-buy/index",
"pages/group-buy/detail",
"pages/second-hand/index",
"pages/second-hand/create",
"pages/notices/index",
"pages/profile/index",
"pages/address/index"
],
"window": {
"navigationBarTitleText": "邻小帮",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f6f7f9"
},
"tabBar": {
"color": "#6b7280",
"selectedColor": "#0f766e",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/home/index",
"text": "首页"
},
{
"pagePath": "pages/group-buy/index",
"text": "团购"
},
{
"pagePath": "pages/second-hand/index",
"text": "闲置"
},
{
"pagePath": "pages/profile/index",
"text": "我的"
}
]
}
}
```
Create `miniapp/miniprogram/app.wxss`.
```css
page {
background: #f6f7f9;
color: #111827;
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
}
.page {
padding: 24rpx;
}
```
Create `miniapp/miniprogram/utils/request.js`.
```js
function request(options) {
const app = getApp()
const token = app.globalData.token
return new Promise((resolve, reject) => {
wx.request({
url: app.globalData.apiBaseUrl + options.url,
method: options.method || 'GET',
data: options.data || {},
header: Object.assign({
Authorization: token
}, options.header || {}),
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300 && res.data && res.data.code === 0) {
resolve(res.data.data)
return
}
reject(new Error((res.data && res.data.message) || '请求失败'))
},
fail: reject
})
})
}
module.exports = {
request
}
```
Create `rider-h5/package.json`.
```json
@@ -482,22 +614,59 @@ Expected: FAIL because enum classes do not exist.
- [ ] **Step 3: Create common API wrappers**
Create `ApiResponse.java`.
Create `ApiResponse.java`. Use a normal Java class because the backend targets Java 8.
```java
package com.linhelp.common.api;
public record ApiResponse<T>(int code, String message, T data) {
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public ApiResponse() {
}
public ApiResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(0, "ok", data);
return new ApiResponse<T>(0, "ok", data);
}
public static ApiResponse<Void> ok() {
return new ApiResponse<>(0, "ok", null);
return new ApiResponse<Void>(0, "ok", null);
}
public static ApiResponse<Void> fail(int code, String message) {
return new ApiResponse<>(code, message, null);
return new ApiResponse<Void>(code, message, null);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
```
@@ -509,7 +678,53 @@ package com.linhelp.common.api;
import java.util.List;
public record PageResponse<T>(long total, long pageNo, long pageSize, List<T> records) {
public class PageResponse<T> {
private long total;
private long pageNo;
private long pageSize;
private List<T> records;
public PageResponse() {
}
public PageResponse(long total, long pageNo, long pageSize, List<T> records) {
this.total = total;
this.pageNo = pageNo;
this.pageSize = pageSize;
this.records = records;
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
public long getPageNo() {
return pageNo;
}
public void setPageNo(long pageNo) {
this.pageNo = pageNo;
}
public long getPageSize() {
return pageSize;
}
public void setPageSize(long pageSize) {
this.pageSize = pageSize;
}
public List<T> getRecords() {
return records;
}
public void setRecords(List<T> records) {
this.records = records;
}
}
```
@@ -520,6 +735,9 @@ Create `GoodsOrderStatus.java`.
```java
package com.linhelp.common.enums;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
@@ -532,16 +750,21 @@ public enum GoodsOrderStatus {
COMPLETED,
CANCELED;
private static final Map<GoodsOrderStatus, Set<GoodsOrderStatus>> TRANSITIONS = Map.of(
PENDING_CONFIRM, Set.of(PREPARING, CANCELED),
PREPARING, Set.of(PENDING_DELIVERY, PENDING_PICKUP, CANCELED),
PENDING_DELIVERY, Set.of(DELIVERING, CANCELED),
PENDING_PICKUP, Set.of(COMPLETED, CANCELED),
DELIVERING, Set.of(COMPLETED, CANCELED)
);
private static final Map<GoodsOrderStatus, Set<GoodsOrderStatus>> TRANSITIONS;
static {
Map<GoodsOrderStatus, Set<GoodsOrderStatus>> transitions = new EnumMap<GoodsOrderStatus, Set<GoodsOrderStatus>>(GoodsOrderStatus.class);
transitions.put(PENDING_CONFIRM, EnumSet.of(PREPARING, CANCELED));
transitions.put(PREPARING, EnumSet.of(PENDING_DELIVERY, PENDING_PICKUP, CANCELED));
transitions.put(PENDING_DELIVERY, EnumSet.of(DELIVERING, CANCELED));
transitions.put(PENDING_PICKUP, EnumSet.of(COMPLETED, CANCELED));
transitions.put(DELIVERING, EnumSet.of(COMPLETED, CANCELED));
TRANSITIONS = Collections.unmodifiableMap(transitions);
}
public boolean canMoveTo(GoodsOrderStatus target) {
return TRANSITIONS.getOrDefault(this, Set.of()).contains(target);
Set<GoodsOrderStatus> targets = TRANSITIONS.get(this);
return targets != null && targets.contains(target);
}
}
```
@@ -1106,7 +1329,22 @@ public final class TenantContext {
SCOPE.remove();
}
private record Scope(Long tenantId, Long communityId) {
private static class Scope {
private final Long tenantId;
private final Long communityId;
private Scope(Long tenantId, Long communityId) {
this.tenantId = tenantId;
this.communityId = communityId;
}
private Long tenantId() {
return tenantId;
}
private Long communityId() {
return communityId;
}
}
}
```
@@ -1118,8 +1356,62 @@ Use Sa-Token for session management. First implementation supports backend accou
Create request/response DTOs:
```java
public record LoginRequest(String username, String password) {}
public record LoginResponse(String token, Long userId, Long tenantId, Long communityId, String roleCode) {}
public class LoginRequest {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
public class LoginResponse {
private String token;
private Long userId;
private Long tenantId;
private Long communityId;
private String roleCode;
public LoginResponse(String token, Long userId, Long tenantId, Long communityId, String roleCode) {
this.token = token;
this.userId = userId;
this.tenantId = tenantId;
this.communityId = communityId;
this.roleCode = roleCode;
}
public String getToken() {
return token;
}
public Long getUserId() {
return userId;
}
public Long getTenantId() {
return tenantId;
}
public Long getCommunityId() {
return communityId;
}
public String getRoleCode() {
return roleCode;
}
}
```
Create `AuthController` endpoints:
@@ -1270,7 +1562,7 @@ void cannotCreateGoodsOrderWhenSkuStockIsInsufficient() {
addressId,
DeliveryMethod.IMMEDIATE,
null,
List.of(new CreateGoodsOrderItemRequest(productId, skuId, 99)),
Collections.singletonList(new CreateGoodsOrderItemRequest(productId, skuId, 99)),
"请尽快送达"
);
@@ -1789,33 +2081,29 @@ git commit -m "feat: add admin web MVP"
### Task 10: Resident Miniapp MVP
**Files:**
- Create: `miniapp/src/main.ts`
- Create: `miniapp/src/App.vue`
- Create: `miniapp/src/pages.json`
- Create: `miniapp/src/api/*.ts`
- Create: `miniapp/src/pages/home/index.vue`
- Create: `miniapp/src/pages/products/index.vue`
- Create: `miniapp/src/pages/products/detail.vue`
- Create: `miniapp/src/pages/orders/index.vue`
- Create: `miniapp/src/pages/orders/detail.vue`
- Create: `miniapp/src/pages/express/create.vue`
- Create: `miniapp/src/pages/group-buy/index.vue`
- Create: `miniapp/src/pages/group-buy/detail.vue`
- Create: `miniapp/src/pages/second-hand/index.vue`
- Create: `miniapp/src/pages/second-hand/create.vue`
- Create: `miniapp/src/pages/notices/index.vue`
- Create: `miniapp/src/pages/profile/index.vue`
- Create: `miniapp/src/pages/address/index.vue`
- Test: `miniapp/src/api/expressApi.test.ts`
- Create: `miniapp/miniprogram/api/http.js`
- Create: `miniapp/miniprogram/api/homeApi.js`
- Create: `miniapp/miniprogram/api/productApi.js`
- Create: `miniapp/miniprogram/api/orderApi.js`
- Create: `miniapp/miniprogram/api/expressApi.js`
- Create: `miniapp/miniprogram/api/groupBuyApi.js`
- Create: `miniapp/miniprogram/api/secondGoodsApi.js`
- Create: `miniapp/miniprogram/api/noticeApi.js`
- Create: `miniapp/miniprogram/api/addressApi.js`
- Create: `miniapp/miniprogram/pages/home/index.js`
- Create: `miniapp/miniprogram/pages/home/index.json`
- Create: `miniapp/miniprogram/pages/home/index.wxml`
- Create: `miniapp/miniprogram/pages/home/index.wxss`
- Create: native page file sets under `miniapp/miniprogram/pages/products`, `orders`, `express`, `group-buy`, `second-hand`, `notices`, `profile`, and `address`
- Test: `miniapp/miniprogram/api/expressApi.test.js`
- [ ] **Step 1: Create express fee UI helper test**
```ts
import { describe, expect, it } from 'vitest'
import { calculateExpressFeeCent } from './expressApi'
```js
const { calculateExpressFeeCent } = require('./expressApi')
describe('calculateExpressFeeCent', () => {
it('matches backend express fee rule', () => {
test('matches backend express fee rule', () => {
expect(calculateExpressFeeCent(1)).toBe(300)
expect(calculateExpressFeeCent(2)).toBe(400)
expect(calculateExpressFeeCent(4)).toBe(600)
@@ -1823,7 +2111,44 @@ describe('calculateExpressFeeCent', () => {
})
```
- [ ] **Step 2: Implement miniapp navigation**
- [ ] **Step 2: Implement native miniapp API modules**
Create `miniapp/miniprogram/api/expressApi.js`.
```js
const { request } = require('../utils/request')
function calculateExpressFeeCent(packageCount) {
if (packageCount <= 0) {
throw new Error('件数必须大于0')
}
return 300 + Math.max(0, packageCount - 1) * 100
}
function createExpressOrder(data) {
return request({
url: '/api/mini/express-orders',
method: 'POST',
data: data
})
}
function listExpressOrders() {
return request({
url: '/api/mini/express-orders'
})
}
module.exports = {
calculateExpressFeeCent,
createExpressOrder,
listExpressOrders
}
```
Create matching CommonJS modules for products, orders, group buy, secondhand, notices, and addresses. Each module exports named functions and calls `request()` from `miniprogram/utils/request.js`.
- [ ] **Step 3: Implement miniapp navigation**
Tabs:
@@ -1845,7 +2170,80 @@ Home shortcuts:
我的订单
```
- [ ] **Step 3: Implement resident flows**
- [ ] **Step 4: Implement native miniapp page pattern**
Use this structure for every page:
```text
index.js page state, lifecycle methods, event handlers
index.json navigation title and page-level component config
index.wxml native miniapp markup
index.wxss page styles
```
Example `miniapp/miniprogram/pages/express/create.js`:
```js
const { calculateExpressFeeCent, createExpressOrder } = require('../../api/expressApi')
Page({
data: {
form: {
expressCompany: '',
pickupCode: '',
pickupAddress: '',
addressId: null,
phone: '',
packageCount: 1,
remark: ''
},
feeCent: 300,
submitting: false
},
onPackageCountChange(event) {
const packageCount = Number(event.detail.value || 1)
this.setData({
'form.packageCount': packageCount,
feeCent: calculateExpressFeeCent(packageCount)
})
},
submit() {
if (this.data.submitting) {
return
}
this.setData({ submitting: true })
createExpressOrder(this.data.form)
.then(() => {
wx.showToast({ title: '已提交' })
wx.navigateBack()
})
.catch((error) => {
wx.showToast({ title: error.message, icon: 'none' })
})
.finally(() => {
this.setData({ submitting: false })
})
}
})
```
Example `miniapp/miniprogram/pages/express/create.wxml`:
```xml
<view class="page">
<input placeholder="快递公司" data-field="expressCompany" />
<input placeholder="取件码" data-field="pickupCode" />
<input placeholder="取件地址" data-field="pickupAddress" />
<input placeholder="手机号" data-field="phone" />
<input type="number" value="{{form.packageCount}}" bindinput="onPackageCountChange" />
<view>预估费用:{{feeCent / 100}}元</view>
<button loading="{{submitting}}" bindtap="submit">提交代取</button>
</view>
```
- [ ] **Step 5: Implement resident flows**
Minimum flows:
@@ -1858,7 +2256,7 @@ Minimum flows:
- Manage addresses and default address.
- View order lists by type and detail pages.
- [ ] **Step 4: Verify**
- [ ] **Step 6: Verify**
Run:
@@ -1866,12 +2264,11 @@ Run:
cd miniapp
npm install
npm test
npm run build
```
Expected: API helper tests pass and `npm run build` produces the WeChat miniapp output under `miniapp/dist/build/mp-weixin`.
Expected: Jest helper tests pass. Open `miniapp/project.config.json` in WeChat DevTools and click Compile; expected result is that the native mini program compiles with no missing page, JSON, WXML, WXSS, or API module errors.
- [ ] **Step 5: Commit**
- [ ] **Step 7: Commit**
```powershell
git add miniapp
@@ -2032,7 +2429,6 @@ npm test
npm run build
cd ..\miniapp
npm test
npm run build
cd ..\rider-h5
npm test
npm run build
@@ -2043,7 +2439,8 @@ Expected:
- Backend tests pass.
- All frontend tests pass.
- All builds succeed.
- Admin web and rider H5 builds succeed.
- Native miniapp helper tests pass, and `miniapp/project.config.json` compiles successfully in WeChat DevTools.
- `git status --short` only shows intended changes before final commit.
- [ ] **Step 5: Commit**
@@ -2073,7 +2470,8 @@ git commit -m "docs: add release runbook and seed data"
The MVP is ready for one-community trial operation when:
- Backend full test suite passes.
- Admin web, miniapp, and rider H5 builds pass.
- Admin web and rider H5 builds pass.
- Native miniapp compiles successfully in WeChat DevTools.
- Manual checklist is completed once against local seed data.
- Admin can move a goods order from submission to completion through dispatch.
- Admin can move an express pickup order from submission to completion through dispatch.