59 KiB
Linxiaobang Phase One Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the phase-one "邻小帮" MVP: one-community operation with SaaS-ready data boundaries, offline payment, admin dispatch, resident miniapp, admin web, and rider H5.
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.
Scope Check
The PRD spans four surfaces: backend API, admin web, resident miniapp, and rider H5. This plan keeps them in one MVP plan because the value is a single testable business loop, but it implements them in separately verifiable milestones:
- Backend domain foundation and API contracts.
- Admin web for management and dispatch.
- Resident miniapp for orders and community content.
- Rider H5 for fulfillment.
- Integration and deployment.
If implementation time needs to be split across teams, Tasks 1-8 should be completed before client-heavy Tasks 9-11.
File Structure
Create this repository structure:
D:\codes\LinHelp
├── backend
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java\com\linhelp
│ │ │ ├── LinHelpApplication.java
│ │ │ ├── common
│ │ │ │ ├── api
│ │ │ │ ├── config
│ │ │ │ ├── enums
│ │ │ │ ├── exception
│ │ │ │ └── security
│ │ │ ├── community
│ │ │ ├── user
│ │ │ ├── merchant
│ │ │ ├── product
│ │ │ ├── order
│ │ │ ├── express
│ │ │ ├── groupbuy
│ │ │ ├── delivery
│ │ │ ├── secondhand
│ │ │ ├── notice
│ │ │ └── storage
│ │ └── resources
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ └── db\migration
│ └── test\java\com\linhelp
├── admin-web
│ ├── package.json
│ ├── vite.config.ts
│ └── src
├── miniapp
│ ├── package.json
│ └── src
├── rider-h5
│ ├── package.json
│ ├── vite.config.ts
│ └── src
├── deploy
│ ├── docker-compose.dev.yml
│ └── nginx
├── docs
│ └── superpowers
└── README.md
Boundary rules:
backend/src/main/java/com/linhelp/commoncontains 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 inlinefetchcalls. - All tables that carry tenant-owned business data include
tenant_idandcommunity_id. - Use table names
app_user,goods_order, andsys_roleinstead of reserved or ambiguous names.
Shared Domain Vocabulary
Use these enum codes in backend and frontend:
GoodsOrderStatus: PENDING_CONFIRM, PREPARING, PENDING_DELIVERY, PENDING_PICKUP, DELIVERING, COMPLETED, CANCELED
ExpressOrderStatus: PENDING_CONFIRM, PENDING_PICKUP, PICKING_UP, DELIVERING, DELIVERED, COMPLETED, CANCELED
GroupBuyOrderStatus: PENDING_CONFIRM, CONFIRMED, PENDING_DELIVERY, PENDING_PICKUP, DELIVERING, COMPLETED, CANCELED
SecondGoodsStatus: PENDING_REVIEW, PUBLISHED, REJECTED, OFF_SHELF, SOLD
DeliveryTaskStatus: ASSIGNED, ACCEPTED, DELIVERING, DELIVERED, EXCEPTION, COMPLETED, CANCELED
OfflinePayStatus: UNPAID, PAID, FREE, EXCEPTION
DeliveryMethod: IMMEDIATE, SCHEDULED, SELF_PICKUP
Task 1: Repository Scaffold And Local Runtime
Files:
-
Create:
backend/pom.xml -
Create:
backend/src/main/java/com/linhelp/LinHelpApplication.java -
Create:
backend/src/main/resources/application.yml -
Create:
backend/src/main/resources/application-dev.yml -
Create:
backend/src/test/java/com/linhelp/LinHelpApplicationTests.java -
Create:
admin-web/package.json -
Create:
admin-web/vite.config.ts -
Create:
miniapp/package.json -
Create:
rider-h5/package.json -
Create:
rider-h5/vite.config.ts -
Create:
deploy/docker-compose.dev.yml -
Create:
.gitignore -
Create:
README.md -
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.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.linhelp</groupId>
<artifactId>linhelp-backend</artifactId>
<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>
<sa-token.version>1.38.0</sa-token.version>
<knife4j.version>4.5.0</knife4j.version>
<minio.version>8.5.11</minio.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</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>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- Step 2: Add Spring Boot entrypoint
Create backend/src/main/java/com/linhelp/LinHelpApplication.java.
package com.linhelp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LinHelpApplication {
public static void main(String[] args) {
SpringApplication.run(LinHelpApplication.class, args);
}
}
- Step 3: Add local infrastructure config
Create deploy/docker-compose.dev.yml.
services:
mysql:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: linhelp
MYSQL_USER: linhelp
MYSQL_PASSWORD: linhelp
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7
ports:
- "6379:6379"
minio:
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: linhelp
MINIO_ROOT_PASSWORD: linhelp123
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
volumes:
mysql_data:
minio_data:
- Step 4: Add backend YAML config
Create backend/src/main/resources/application.yml.
spring:
profiles:
active: dev
application:
name: linhelp-backend
server:
port: 8080
knife4j:
enable: true
Create backend/src/main/resources/application-dev.yml.
spring:
datasource:
url: jdbc:mysql://localhost:3306/linhelp?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: linhelp
password: linhelp
data:
redis:
host: localhost
port: 6379
flyway:
enabled: true
locations: classpath:db/migration
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
sa-token:
token-name: Authorization
timeout: 2592000
linhelp:
storage:
endpoint: http://localhost:9000
access-key: linhelp
secret-key: linhelp123
bucket: linhelp
- Step 5: Add minimal frontend package files
Create admin-web/package.json.
{
"name": "linhelp-admin-web",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 5173",
"build": "vue-tsc -b && vite build",
"test": "vitest run",
"lint": "eslint ."
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.2",
"element-plus": "^2.7.6",
"pinia": "^2.1.7",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"typescript": "^5.5.3",
"vite": "^5.3.3",
"vitest": "^2.0.2",
"vue-tsc": "^2.0.26"
}
}
Create miniapp/package.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"
},
"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"
}
}
Create rider-h5/package.json.
{
"name": "linhelp-rider-h5",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 5174",
"build": "vue-tsc -b && vite build",
"test": "vitest run",
"lint": "eslint ."
},
"dependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"axios": "^1.7.2",
"pinia": "^2.1.7",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
},
"devDependencies": {
"typescript": "^5.5.3",
"vite": "^5.3.3",
"vitest": "^2.0.2",
"vue-tsc": "^2.0.26"
}
}
- Step 6: Verify scaffold
Run:
docker compose -f deploy/docker-compose.dev.yml up -d
cd backend
mvn test
Expected: MySQL, Redis, and MinIO containers start; mvn test passes or reports no failing tests.
- Step 7: Commit
git add .gitignore README.md deploy backend admin-web miniapp rider-h5
git commit -m "chore: scaffold linhelp applications"
Task 2: Database Schema, Common API Shape, And Enums
Files:
-
Create:
backend/src/main/resources/db/migration/V1__init_schema.sql -
Create:
backend/src/main/java/com/linhelp/common/api/ApiResponse.java -
Create:
backend/src/main/java/com/linhelp/common/api/PageResponse.java -
Create:
backend/src/main/java/com/linhelp/common/exception/BizException.java -
Create:
backend/src/main/java/com/linhelp/common/exception/GlobalExceptionHandler.java -
Create:
backend/src/main/java/com/linhelp/common/enums/*.java -
Test:
backend/src/test/java/com/linhelp/common/enums/StatusTransitionTests.java -
Step 1: Write enum transition tests
Create StatusTransitionTests.java.
package com.linhelp.common.enums;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class StatusTransitionTests {
@Test
void goodsOrderCanMoveFromPendingConfirmToPreparing() {
assertThat(GoodsOrderStatus.PENDING_CONFIRM.canMoveTo(GoodsOrderStatus.PREPARING)).isTrue();
}
@Test
void goodsOrderCannotMoveFromCompletedToDelivering() {
assertThat(GoodsOrderStatus.COMPLETED.canMoveTo(GoodsOrderStatus.DELIVERING)).isFalse();
}
@Test
void expressOrderCanMoveThroughPickupFlow() {
assertThat(ExpressOrderStatus.PENDING_CONFIRM.canMoveTo(ExpressOrderStatus.PENDING_PICKUP)).isTrue();
assertThat(ExpressOrderStatus.PENDING_PICKUP.canMoveTo(ExpressOrderStatus.PICKING_UP)).isTrue();
assertThat(ExpressOrderStatus.PICKING_UP.canMoveTo(ExpressOrderStatus.DELIVERING)).isTrue();
}
}
- Step 2: Run tests to verify compile failure
Run:
cd backend
mvn -Dtest=StatusTransitionTests test
Expected: FAIL because enum classes do not exist.
- Step 3: Create common API wrappers
Create ApiResponse.java.
package com.linhelp.common.api;
public record ApiResponse<T>(int code, String message, T data) {
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(0, "ok", data);
}
public static ApiResponse<Void> ok() {
return new ApiResponse<>(0, "ok", null);
}
public static ApiResponse<Void> fail(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}
Create PageResponse.java.
package com.linhelp.common.api;
import java.util.List;
public record PageResponse<T>(long total, long pageNo, long pageSize, List<T> records) {
}
- Step 4: Create status enums with transition rules
Create GoodsOrderStatus.java.
package com.linhelp.common.enums;
import java.util.Map;
import java.util.Set;
public enum GoodsOrderStatus {
PENDING_CONFIRM,
PREPARING,
PENDING_DELIVERY,
PENDING_PICKUP,
DELIVERING,
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)
);
public boolean canMoveTo(GoodsOrderStatus target) {
return TRANSITIONS.getOrDefault(this, Set.of()).contains(target);
}
}
Create the remaining enum files with matching names from "Shared Domain Vocabulary". Give ExpressOrderStatus the same canMoveTo method and a transition map for PENDING_CONFIRM -> PENDING_PICKUP -> PICKING_UP -> DELIVERING -> DELIVERED -> COMPLETED, with cancellation allowed before completion.
- Step 5: Create initial schema
Create backend/src/main/resources/db/migration/V1__init_schema.sql.
CREATE TABLE tenant (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(80) NOT NULL,
status VARCHAR(24) NOT NULL DEFAULT 'ENABLED',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0
);
CREATE TABLE community (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
name VARCHAR(80) NOT NULL,
address VARCHAR(255) NOT NULL,
contact_name VARCHAR(40),
contact_phone VARCHAR(32),
status VARCHAR(24) NOT NULL DEFAULT 'ENABLED',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_community_tenant (tenant_id)
);
CREATE TABLE community_module_config (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
module_code VARCHAR(40) NOT NULL,
enabled TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_community_module (community_id, module_code)
);
CREATE TABLE app_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
openid VARCHAR(80),
nickname VARCHAR(80),
avatar_url VARCHAR(255),
phone VARCHAR(32),
status VARCHAR(24) NOT NULL DEFAULT 'ENABLED',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_app_user_openid (openid),
INDEX idx_app_user_community (community_id)
);
CREATE TABLE user_address (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
contact_name VARCHAR(40) NOT NULL,
phone VARCHAR(32) NOT NULL,
building VARCHAR(80) NOT NULL,
room VARCHAR(80) NOT NULL,
detail VARCHAR(255),
is_default TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_user_address_user (user_id)
);
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(40) NOT NULL UNIQUE,
name VARCHAR(80) NOT NULL
);
CREATE TABLE user_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
role_code VARCHAR(40) NOT NULL,
tenant_id BIGINT NOT NULL,
community_id BIGINT,
merchant_id BIGINT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_role_scope (user_id, role_code, tenant_id, community_id, merchant_id)
);
CREATE TABLE merchant (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
merchant_type VARCHAR(40) NOT NULL,
contact_name VARCHAR(40),
contact_phone VARCHAR(32),
status VARCHAR(24) NOT NULL DEFAULT 'ENABLED',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_merchant_community (community_id)
);
CREATE TABLE product_category (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
name VARCHAR(80) NOT NULL,
sort_no INT NOT NULL DEFAULT 0,
enabled TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0
);
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
merchant_id BIGINT NOT NULL,
category_id BIGINT NOT NULL,
name VARCHAR(120) NOT NULL,
cover_url VARCHAR(255),
description TEXT,
unit_name VARCHAR(20) NOT NULL DEFAULT '件',
status VARCHAR(24) NOT NULL DEFAULT 'OFF_SHELF',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_product_category (category_id),
INDEX idx_product_merchant (merchant_id)
);
CREATE TABLE product_sku (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
sku_name VARCHAR(80) NOT NULL,
price_cent INT NOT NULL,
origin_price_cent INT,
stock INT NOT NULL DEFAULT 0,
enabled TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_product_sku_product (product_id)
);
CREATE TABLE goods_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(40) NOT NULL UNIQUE,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
merchant_id BIGINT NOT NULL,
address_id BIGINT,
delivery_method VARCHAR(24) NOT NULL,
scheduled_time DATETIME,
status VARCHAR(32) NOT NULL,
offline_pay_status VARCHAR(24) NOT NULL DEFAULT 'UNPAID',
total_amount_cent INT NOT NULL,
remark VARCHAR(255),
cancel_reason VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_goods_order_user (user_id),
INDEX idx_goods_order_status (community_id, status)
);
CREATE TABLE goods_order_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
product_name VARCHAR(120) NOT NULL,
sku_name VARCHAR(80) NOT NULL,
quantity INT NOT NULL,
price_cent INT NOT NULL,
amount_cent INT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_goods_order_item_order (order_id)
);
CREATE TABLE express_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(40) NOT NULL UNIQUE,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
address_id BIGINT NOT NULL,
express_company VARCHAR(80) NOT NULL,
pickup_code VARCHAR(120) NOT NULL,
pickup_address VARCHAR(255) NOT NULL,
package_count INT NOT NULL,
service_fee_cent INT NOT NULL,
phone VARCHAR(32) NOT NULL,
image_url VARCHAR(255),
status VARCHAR(32) NOT NULL,
offline_pay_status VARCHAR(24) NOT NULL DEFAULT 'UNPAID',
remark VARCHAR(255),
cancel_reason VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_express_order_status (community_id, status)
);
CREATE TABLE delivery_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
name VARCHAR(40) NOT NULL,
phone VARCHAR(32) NOT NULL,
status VARCHAR(24) NOT NULL DEFAULT 'ENABLED',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0
);
CREATE TABLE delivery_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
biz_type VARCHAR(32) NOT NULL,
biz_order_id BIGINT NOT NULL,
delivery_user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
delivered_photo_url VARCHAR(255),
exception_type VARCHAR(64),
exception_note VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
UNIQUE KEY uk_delivery_biz_order (biz_type, biz_order_id),
INDEX idx_delivery_user_status (delivery_user_id, status)
);
Append these SQL table definitions to V1__init_schema.sql.
CREATE TABLE stock_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
change_quantity INT NOT NULL,
before_stock INT NOT NULL,
after_stock INT NOT NULL,
biz_type VARCHAR(32) NOT NULL,
biz_order_id BIGINT,
note VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_stock_log_sku (sku_id)
);
CREATE TABLE order_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
biz_type VARCHAR(32) NOT NULL,
biz_order_id BIGINT NOT NULL,
from_status VARCHAR(32),
to_status VARCHAR(32),
action VARCHAR(64) NOT NULL,
operator_id BIGINT,
note VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_order_log_biz (biz_type, biz_order_id)
);
CREATE TABLE express_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
express_order_id BIGINT NOT NULL,
from_status VARCHAR(32),
to_status VARCHAR(32),
action VARCHAR(64) NOT NULL,
operator_id BIGINT,
note VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_express_log_order (express_order_id)
);
CREATE TABLE group_buy (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
merchant_id BIGINT NOT NULL,
title VARCHAR(120) NOT NULL,
cover_url VARCHAR(255),
description TEXT,
price_cent INT NOT NULL,
origin_price_cent INT,
stock INT NOT NULL,
deadline_at DATETIME NOT NULL,
expected_delivery_at DATETIME,
status VARCHAR(32) NOT NULL DEFAULT 'NOT_STARTED',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_group_buy_status (community_id, status)
);
CREATE TABLE group_buy_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(40) NOT NULL UNIQUE,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
group_buy_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
address_id BIGINT,
delivery_method VARCHAR(24) NOT NULL,
quantity INT NOT NULL,
amount_cent INT NOT NULL,
status VARCHAR(32) NOT NULL,
offline_pay_status VARCHAR(24) NOT NULL DEFAULT 'UNPAID',
remark VARCHAR(255),
cancel_reason VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_group_buy_order_activity (group_buy_id),
INDEX idx_group_buy_order_user (user_id)
);
CREATE TABLE delivery_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
delivery_order_id BIGINT NOT NULL,
from_status VARCHAR(32),
to_status VARCHAR(32),
action VARCHAR(64) NOT NULL,
operator_id BIGINT,
note VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_delivery_log_order (delivery_order_id)
);
CREATE TABLE second_goods (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
title VARCHAR(120) NOT NULL,
price_cent INT NOT NULL,
description TEXT,
category VARCHAR(40) NOT NULL,
contact_phone VARCHAR(32) NOT NULL,
trade_method VARCHAR(32) NOT NULL,
status VARCHAR(32) NOT NULL,
reject_reason VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_second_goods_status (community_id, status),
INDEX idx_second_goods_user (user_id)
);
CREATE TABLE second_goods_img (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
second_goods_id BIGINT NOT NULL,
image_url VARCHAR(255) NOT NULL,
sort_no INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_second_goods_img_goods (second_goods_id)
);
CREATE TABLE second_goods_comment (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
second_goods_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
content VARCHAR(500) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_second_goods_comment_goods (second_goods_id)
);
CREATE TABLE notice (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
title VARCHAR(120) NOT NULL,
content TEXT NOT NULL,
category VARCHAR(40) NOT NULL,
pinned TINYINT NOT NULL DEFAULT 0,
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT',
published_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_notice_status (community_id, status, pinned)
);
CREATE TABLE banner (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
title VARCHAR(120) NOT NULL,
image_url VARCHAR(255) NOT NULL,
link_type VARCHAR(32),
link_value VARCHAR(255),
sort_no INT NOT NULL DEFAULT 0,
enabled TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_banner_community (community_id, enabled)
);
CREATE TABLE feedback (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
biz_type VARCHAR(32),
biz_order_id BIGINT,
content VARCHAR(500) NOT NULL,
status VARCHAR(24) NOT NULL DEFAULT 'OPEN',
reply VARCHAR(500),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_feedback_status (community_id, status)
);
CREATE TABLE payment_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
biz_type VARCHAR(32) NOT NULL,
biz_order_id BIGINT NOT NULL,
amount_cent INT NOT NULL,
pay_method VARCHAR(32) NOT NULL DEFAULT 'OFFLINE',
pay_status VARCHAR(24) NOT NULL,
paid_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_payment_biz (biz_type, biz_order_id)
);
CREATE TABLE refund_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
community_id BIGINT NOT NULL,
payment_record_id BIGINT,
biz_type VARCHAR(32) NOT NULL,
biz_order_id BIGINT NOT NULL,
amount_cent INT NOT NULL,
refund_status VARCHAR(24) NOT NULL,
reason VARCHAR(255),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_refund_biz (biz_type, biz_order_id)
);
- Step 6: Run migration and tests
Run:
docker compose -f deploy/docker-compose.dev.yml up -d
cd backend
mvn test
Expected: Flyway applies V1__init_schema.sql; enum tests pass.
- Step 7: Commit
git add backend/src/main/resources/db/migration backend/src/main/java/com/linhelp/common backend/src/test/java/com/linhelp/common
git commit -m "feat: add core schema and status enums"
Task 3: Authentication, Tenant Context, And Permissions
Files:
-
Create:
backend/src/main/java/com/linhelp/common/security/TenantContext.java -
Create:
backend/src/main/java/com/linhelp/common/security/CurrentUser.java -
Create:
backend/src/main/java/com/linhelp/common/security/AuthController.java -
Create:
backend/src/main/java/com/linhelp/common/security/AuthService.java -
Create:
backend/src/main/java/com/linhelp/common/security/RequireRole.java -
Create:
backend/src/main/java/com/linhelp/common/config/SaTokenConfig.java -
Test:
backend/src/test/java/com/linhelp/common/security/TenantContextTests.java -
Step 1: Write tenant context tests
Create TenantContextTests.java.
package com.linhelp.common.security;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TenantContextTests {
@AfterEach
void clear() {
TenantContext.clear();
}
@Test
void storesTenantAndCommunityForCurrentRequestThread() {
TenantContext.set(1L, 10L);
assertThat(TenantContext.tenantId()).isEqualTo(1L);
assertThat(TenantContext.communityId()).isEqualTo(10L);
}
}
- Step 2: Implement tenant context
Create TenantContext.java.
package com.linhelp.common.security;
public final class TenantContext {
private static final ThreadLocal<Scope> SCOPE = new ThreadLocal<>();
private TenantContext() {
}
public static void set(Long tenantId, Long communityId) {
SCOPE.set(new Scope(tenantId, communityId));
}
public static Long tenantId() {
return SCOPE.get() == null ? null : SCOPE.get().tenantId();
}
public static Long communityId() {
return SCOPE.get() == null ? null : SCOPE.get().communityId();
}
public static void clear() {
SCOPE.remove();
}
private record Scope(Long tenantId, Long communityId) {
}
}
- Step 3: Implement simple admin login
Use Sa-Token for session management. First implementation supports backend account login for admin, merchant, and rider users. WeChat miniapp login can initially accept a dev openid in local mode and be replaced with jscode2session in a later integration task.
Create request/response DTOs:
public record LoginRequest(String username, String password) {}
public record LoginResponse(String token, Long userId, Long tenantId, Long communityId, String roleCode) {}
Create AuthController endpoints:
POST /api/auth/admin-login
POST /api/auth/dev-miniapp-login
POST /api/auth/logout
GET /api/auth/me
- Step 4: Add role guard
Create RequireRole.java as an annotation and add an interceptor or Sa-Token route check so admin endpoints require one of:
PLATFORM_ADMIN
COMMUNITY_ADMIN
MERCHANT
RIDER
- Step 5: Verify
Run:
cd backend
mvn -Dtest=TenantContextTests test
mvn test
Expected: tenant tests and full backend tests pass.
- Step 6: Commit
git add backend/src/main/java/com/linhelp/common/security backend/src/main/java/com/linhelp/common/config backend/src/test/java/com/linhelp/common/security
git commit -m "feat: add authentication and tenant context"
Task 4: Community, Notices, Banners, Users, And Addresses
Files:
-
Create:
backend/src/main/java/com/linhelp/community/* -
Create:
backend/src/main/java/com/linhelp/notice/* -
Create:
backend/src/main/java/com/linhelp/user/* -
Test:
backend/src/test/java/com/linhelp/user/UserAddressServiceTests.java -
Test:
backend/src/test/java/com/linhelp/notice/NoticeServiceTests.java -
Step 1: Write address default behavior test
@Test
void settingOneAddressAsDefaultClearsOtherDefaultsForSameUser() {
addressService.create(userId, request("张三", "13800000000", "1栋", "101", true));
AddressResponse second = addressService.create(userId, request("张三", "13800000000", "1栋", "102", true));
List<AddressResponse> addresses = addressService.listMine(userId);
assertThat(addresses).filteredOn(AddressResponse::isDefault).singleElement()
.extracting(AddressResponse::id).isEqualTo(second.id());
}
- Step 2: Implement user address APIs
Create endpoints:
GET /api/mini/addresses
POST /api/mini/addresses
PUT /api/mini/addresses/{id}
DELETE /api/mini/addresses/{id}
PUT /api/mini/addresses/{id}/default
Validation:
-
contactNamelength 2-40. -
phonematches mainland mobile format or local service phone format. -
buildingandroomare required. -
Users can only modify their own addresses in the current community.
-
Step 3: Implement notice and banner APIs
Admin endpoints:
GET /api/admin/notices
POST /api/admin/notices
PUT /api/admin/notices/{id}
PUT /api/admin/notices/{id}/publish
PUT /api/admin/notices/{id}/offline
DELETE /api/admin/notices/{id}
GET /api/admin/banners
POST /api/admin/banners
PUT /api/admin/banners/{id}
DELETE /api/admin/banners/{id}
Miniapp endpoints:
GET /api/mini/notices
GET /api/mini/notices/{id}
GET /api/mini/banners
- Step 4: Verify
Run:
cd backend
mvn -Dtest=UserAddressServiceTests,NoticeServiceTests test
mvn test
Expected: tests pass and OpenAPI shows address, notice, and banner groups.
- Step 5: Commit
git add backend/src/main/java/com/linhelp/community backend/src/main/java/com/linhelp/notice backend/src/main/java/com/linhelp/user backend/src/test/java/com/linhelp/user backend/src/test/java/com/linhelp/notice
git commit -m "feat: add community content and address APIs"
Task 5: Product Catalog, SKU Inventory, And Goods Orders
Files:
-
Create:
backend/src/main/java/com/linhelp/merchant/* -
Create:
backend/src/main/java/com/linhelp/product/* -
Create:
backend/src/main/java/com/linhelp/order/* -
Test:
backend/src/test/java/com/linhelp/order/GoodsOrderServiceTests.java -
Test:
backend/src/test/java/com/linhelp/product/ProductInventoryTests.java -
Step 1: Write inventory and order tests
@Test
void cannotCreateGoodsOrderWhenSkuStockIsInsufficient() {
CreateGoodsOrderRequest request = new CreateGoodsOrderRequest(
addressId,
DeliveryMethod.IMMEDIATE,
null,
List.of(new CreateGoodsOrderItemRequest(productId, skuId, 99)),
"请尽快送达"
);
assertThatThrownBy(() -> goodsOrderService.create(userId, request))
.hasMessageContaining("库存不足");
}
@Test
void selfPickupOrderMovesToPendingPickupAfterPreparing() {
GoodsOrderResponse order = goodsOrderService.create(userId, selfPickupRequest());
goodsOrderService.confirm(order.id());
goodsOrderService.markPrepared(order.id());
assertThat(goodsOrderService.detail(order.id()).status()).isEqualTo(GoodsOrderStatus.PENDING_PICKUP);
}
- Step 2: Implement merchant and product management APIs
Admin endpoints:
GET /api/admin/merchants
POST /api/admin/merchants
PUT /api/admin/merchants/{id}
PUT /api/admin/merchants/{id}/enable
PUT /api/admin/merchants/{id}/disable
GET /api/admin/product-categories
POST /api/admin/product-categories
PUT /api/admin/product-categories/{id}
DELETE /api/admin/product-categories/{id}
GET /api/admin/products
POST /api/admin/products
PUT /api/admin/products/{id}
PUT /api/admin/products/{id}/on-shelf
PUT /api/admin/products/{id}/off-shelf
POST /api/admin/products/{id}/skus
PUT /api/admin/product-skus/{id}
Miniapp endpoints:
GET /api/mini/product-categories
GET /api/mini/products
GET /api/mini/products/{id}
- Step 3: Implement goods order creation
Endpoint:
POST /api/mini/goods-orders
GET /api/mini/goods-orders
GET /api/mini/goods-orders/{id}
PUT /api/mini/goods-orders/{id}/cancel
PUT /api/mini/goods-orders/{id}/complete
Creation rules:
-
Offline payment status starts as
UNPAID. -
Status starts as
PENDING_CONFIRM. -
Total amount is calculated from current SKU prices.
-
Stock is deducted at creation and recorded in
stock_log. -
Scheduled delivery requires
scheduledTime. -
Self pickup does not create a delivery task.
-
Step 4: Implement admin goods order operations
Endpoints:
GET /api/admin/goods-orders
GET /api/admin/goods-orders/{id}
PUT /api/admin/goods-orders/{id}/confirm
PUT /api/admin/goods-orders/{id}/prepared
PUT /api/admin/goods-orders/{id}/cancel
PUT /api/admin/goods-orders/{id}/offline-pay-status
- Step 5: Verify
Run:
cd backend
mvn -Dtest=ProductInventoryTests,GoodsOrderServiceTests test
mvn test
Expected: inventory, self-pickup, order status, and total amount tests pass.
- Step 6: Commit
git add backend/src/main/java/com/linhelp/merchant backend/src/main/java/com/linhelp/product backend/src/main/java/com/linhelp/order backend/src/test/java/com/linhelp/product backend/src/test/java/com/linhelp/order
git commit -m "feat: add product catalog and goods orders"
Task 6: Express Pickup Orders
Files:
-
Create:
backend/src/main/java/com/linhelp/express/* -
Test:
backend/src/test/java/com/linhelp/express/ExpressFeeCalculatorTests.java -
Test:
backend/src/test/java/com/linhelp/express/ExpressOrderServiceTests.java -
Step 1: Write express fee tests
@Test
void firstPackageCostsThreeYuan() {
assertThat(ExpressFeeCalculator.calculateCent(1)).isEqualTo(300);
}
@Test
void extraPackagesCostOneYuanEach() {
assertThat(ExpressFeeCalculator.calculateCent(3)).isEqualTo(500);
}
@Test
void packageCountMustBePositive() {
assertThatThrownBy(() -> ExpressFeeCalculator.calculateCent(0))
.hasMessageContaining("件数必须大于0");
}
- Step 2: Implement fee calculator
package com.linhelp.express;
public final class ExpressFeeCalculator {
private ExpressFeeCalculator() {
}
public static int calculateCent(int packageCount) {
if (packageCount <= 0) {
throw new IllegalArgumentException("件数必须大于0");
}
return 300 + Math.max(0, packageCount - 1) * 100;
}
}
- Step 3: Implement miniapp express APIs
Endpoints:
POST /api/mini/express-orders
GET /api/mini/express-orders
GET /api/mini/express-orders/{id}
PUT /api/mini/express-orders/{id}/cancel
PUT /api/mini/express-orders/{id}/complete
Validation:
-
Express company is required.
-
Pickup code is required.
-
Pickup address is required.
-
Address ID must belong to current user.
-
Package count must be greater than zero.
-
Step 4: Implement admin express APIs
Endpoints:
GET /api/admin/express-orders
GET /api/admin/express-orders/{id}
PUT /api/admin/express-orders/{id}/confirm
PUT /api/admin/express-orders/{id}/adjust-fee
PUT /api/admin/express-orders/{id}/cancel
PUT /api/admin/express-orders/{id}/offline-pay-status
Rules:
-
Confirmed order moves from
PENDING_CONFIRMtoPENDING_PICKUP. -
Fee adjustment writes an
express_log. -
Canceled order records cancel reason.
-
Step 5: Verify
Run:
cd backend
mvn -Dtest=ExpressFeeCalculatorTests,ExpressOrderServiceTests test
mvn test
Expected: fee calculation and status tests pass.
- Step 6: Commit
git add backend/src/main/java/com/linhelp/express backend/src/test/java/com/linhelp/express
git commit -m "feat: add express pickup orders"
Task 7: Delivery Assignment And Rider APIs
Files:
-
Create:
backend/src/main/java/com/linhelp/delivery/* -
Test:
backend/src/test/java/com/linhelp/delivery/DeliveryAssignmentServiceTests.java -
Test:
backend/src/test/java/com/linhelp/delivery/RiderTaskServiceTests.java -
Step 1: Write assignment tests
@Test
void adminCanAssignPendingDeliveryGoodsOrderToRider() {
GoodsOrderResponse order = preparedDeliveryOrder();
DeliveryOrderResponse task = deliveryAssignmentService.assignGoodsOrder(order.id(), riderId);
assertThat(task.bizType()).isEqualTo("GOODS_ORDER");
assertThat(task.status()).isEqualTo(DeliveryTaskStatus.ASSIGNED);
}
@Test
void cannotAssignSelfPickupGoodsOrder() {
GoodsOrderResponse order = preparedSelfPickupOrder();
assertThatThrownBy(() -> deliveryAssignmentService.assignGoodsOrder(order.id(), riderId))
.hasMessageContaining("自提订单无需派单");
}
- Step 2: Implement delivery admin APIs
Endpoints:
GET /api/admin/delivery-users
POST /api/admin/delivery-users
PUT /api/admin/delivery-users/{id}
PUT /api/admin/delivery-users/{id}/enable
PUT /api/admin/delivery-users/{id}/disable
POST /api/admin/delivery-orders/assign-goods-order
POST /api/admin/delivery-orders/assign-express-order
POST /api/admin/delivery-orders/assign-group-buy-order
GET /api/admin/delivery-orders
GET /api/admin/delivery-orders/{id}
- Step 3: Implement rider H5 APIs
Endpoints:
GET /api/rider/tasks
GET /api/rider/tasks/{id}
PUT /api/rider/tasks/{id}/start
PUT /api/rider/tasks/{id}/delivered
PUT /api/rider/tasks/{id}/exception
GET /api/rider/income-summary
Rules:
-
Rider only sees assigned tasks for their own
delivery_user_id. -
Starting a goods order moves the linked goods order to
DELIVERING. -
Starting an express order moves
PENDING_PICKUPtoPICKING_UP, and laterPICKING_UPtoDELIVERINGwhen package is marked picked. -
Delivered task stores
delivered_photo_url. -
Exception task stores
exception_typeandexception_note. -
Step 4: Verify
Run:
cd backend
mvn -Dtest=DeliveryAssignmentServiceTests,RiderTaskServiceTests test
mvn test
Expected: assignment, permission, and status sync tests pass.
- Step 5: Commit
git add backend/src/main/java/com/linhelp/delivery backend/src/test/java/com/linhelp/delivery
git commit -m "feat: add delivery assignment and rider APIs"
Task 8: Group Buy, Secondhand, Storage, And Feedback
Files:
-
Create:
backend/src/main/java/com/linhelp/groupbuy/* -
Create:
backend/src/main/java/com/linhelp/secondhand/* -
Create:
backend/src/main/java/com/linhelp/storage/* -
Create:
backend/src/main/java/com/linhelp/feedback/* -
Test:
backend/src/test/java/com/linhelp/groupbuy/GroupBuyServiceTests.java -
Test:
backend/src/test/java/com/linhelp/secondhand/SecondGoodsReviewTests.java -
Step 1: Write group buy tests
@Test
void cannotOrderAfterGroupBuyDeadline() {
GroupBuyResponse groupBuy = groupBuyFixture.closedGroupBuy();
assertThatThrownBy(() -> groupBuyOrderService.create(userId, requestFor(groupBuy.id())))
.hasMessageContaining("团购已截单");
}
@Test
void groupBuyOrderCanBePreparedForSelfPickup() {
GroupBuyOrderResponse order = groupBuyOrderService.create(userId, selfPickupGroupBuyRequest());
groupBuyOrderService.confirm(order.id());
groupBuyOrderService.markReady(order.id());
assertThat(groupBuyOrderService.detail(order.id()).status()).isEqualTo(GroupBuyOrderStatus.PENDING_PICKUP);
}
- Step 2: Implement storage APIs
Endpoint:
POST /api/files/upload
Rules:
-
Accept image MIME types only for phase one.
-
Store object with path prefix
tenant/{tenantId}/community/{communityId}/yyyy/MM/dd/. -
Return public or signed URL according to MinIO config.
-
Step 3: Implement group buy APIs
Admin endpoints:
GET /api/admin/group-buys
POST /api/admin/group-buys
PUT /api/admin/group-buys/{id}
PUT /api/admin/group-buys/{id}/start
PUT /api/admin/group-buys/{id}/close
PUT /api/admin/group-buys/{id}/complete
GET /api/admin/group-buys/{id}/orders
Miniapp endpoints:
GET /api/mini/group-buys
GET /api/mini/group-buys/{id}
POST /api/mini/group-buy-orders
GET /api/mini/group-buy-orders
GET /api/mini/group-buy-orders/{id}
PUT /api/mini/group-buy-orders/{id}/cancel
PUT /api/mini/group-buy-orders/{id}/complete
- Step 4: Implement secondhand APIs
Admin endpoints:
GET /api/admin/second-goods
PUT /api/admin/second-goods/{id}/approve
PUT /api/admin/second-goods/{id}/reject
PUT /api/admin/second-goods/{id}/off-shelf
Miniapp endpoints:
GET /api/mini/second-goods
GET /api/mini/second-goods/{id}
POST /api/mini/second-goods
PUT /api/mini/second-goods/{id}/off-shelf
PUT /api/mini/second-goods/{id}/sold
POST /api/mini/second-goods/{id}/comments
Rules:
-
New listing starts as
PENDING_REVIEW. -
Only
PUBLISHEDlistings are visible in public miniapp list. -
Seller can off-shelf or mark sold for their own listing.
-
Admin rejection requires reason.
-
Step 5: Verify
Run:
cd backend
mvn -Dtest=GroupBuyServiceTests,SecondGoodsReviewTests test
mvn test
Expected: group buy cutoff, self-pickup, listing review, and visibility tests pass.
- Step 6: Commit
git add backend/src/main/java/com/linhelp/groupbuy backend/src/main/java/com/linhelp/secondhand backend/src/main/java/com/linhelp/storage backend/src/main/java/com/linhelp/feedback backend/src/test/java/com/linhelp/groupbuy backend/src/test/java/com/linhelp/secondhand
git commit -m "feat: add group buy secondhand and file storage"
Task 9: Admin Web MVP
Files:
-
Create:
admin-web/src/main.ts -
Create:
admin-web/src/App.vue -
Create:
admin-web/src/router/index.ts -
Create:
admin-web/src/api/http.ts -
Create:
admin-web/src/api/*.ts -
Create:
admin-web/src/layout/AdminLayout.vue -
Create:
admin-web/src/views/LoginView.vue -
Create:
admin-web/src/views/DashboardView.vue -
Create:
admin-web/src/views/ProductListView.vue -
Create:
admin-web/src/views/GoodsOrderListView.vue -
Create:
admin-web/src/views/ExpressOrderListView.vue -
Create:
admin-web/src/views/DeliveryDispatchView.vue -
Create:
admin-web/src/views/GroupBuyListView.vue -
Create:
admin-web/src/views/SecondGoodsReviewView.vue -
Create:
admin-web/src/views/NoticeListView.vue -
Test:
admin-web/src/api/orderApi.test.ts -
Step 1: Create typed API client tests
import { describe, expect, it } from 'vitest'
import { buildGoodsOrderQuery } from './orderApi'
describe('buildGoodsOrderQuery', () => {
it('keeps status and date range filters stable', () => {
expect(buildGoodsOrderQuery({
status: 'PENDING_DELIVERY',
startDate: '2026-07-01',
endDate: '2026-07-07'
})).toEqual({
status: 'PENDING_DELIVERY',
startDate: '2026-07-01',
endDate: '2026-07-07'
})
})
})
- Step 2: Implement admin shell
Routes:
/login
/
/products
/goods-orders
/express-orders
/dispatch
/group-buys
/second-goods
/notices
Layout:
-
Left navigation.
-
Top bar with current community and logout.
-
Main content uses Element Plus table/form patterns.
-
Step 3: Implement management screens
Screen minimums:
-
Dashboard: today orders, receivable amount, pending confirmation, pending delivery, express count.
-
Product list: search, create, edit, SKU stock, on-shelf/off-shelf.
-
Goods order list: filter, detail drawer, confirm, prepared, cancel, offline payment status.
-
Express order list: filter, detail drawer, confirm, adjust fee, cancel, offline payment status.
-
Dispatch: tabs for goods, express, group buy; choose rider; assign.
-
Group buy list: create, edit, start, close, complete, order summary.
-
Second goods review: approve, reject with reason, off-shelf.
-
Notice list: create, edit, publish, offline, delete.
-
Step 4: Verify
Run:
cd admin-web
npm install
npm test
npm run build
Expected: Vitest passes and Vite build succeeds.
- Step 5: Commit
git add admin-web
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 -
Step 1: Create express fee UI helper test
import { describe, expect, it } from 'vitest'
import { calculateExpressFeeCent } from './expressApi'
describe('calculateExpressFeeCent', () => {
it('matches backend express fee rule', () => {
expect(calculateExpressFeeCent(1)).toBe(300)
expect(calculateExpressFeeCent(2)).toBe(400)
expect(calculateExpressFeeCent(4)).toBe(600)
})
})
- Step 2: Implement miniapp navigation
Tabs:
首页
团购
闲置
我的
Home shortcuts:
商品预定
代取快递
今日团购
二手闲置
社区公告
我的订单
- Step 3: Implement resident flows
Minimum flows:
-
Browse home banners, notices, hot products, group buys, and secondhand listings.
-
Browse product categories and product detail.
-
Submit goods order with delivery method, address, scheduled time, and remark.
-
Submit express pickup order with package count and auto fee display.
-
Submit group buy order.
-
Publish secondhand listing and show pending review state.
-
Manage addresses and default address.
-
View order lists by type and detail pages.
-
Step 4: Verify
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.
- Step 5: Commit
git add miniapp
git commit -m "feat: add resident miniapp MVP"
Task 11: Rider H5 MVP
Files:
-
Create:
rider-h5/src/main.ts -
Create:
rider-h5/src/App.vue -
Create:
rider-h5/src/router/index.ts -
Create:
rider-h5/src/api/http.ts -
Create:
rider-h5/src/api/riderTaskApi.ts -
Create:
rider-h5/src/views/LoginView.vue -
Create:
rider-h5/src/views/TaskListView.vue -
Create:
rider-h5/src/views/TaskDetailView.vue -
Create:
rider-h5/src/views/IncomeSummaryView.vue -
Test:
rider-h5/src/api/riderTaskApi.test.ts -
Step 1: Write task grouping test
import { describe, expect, it } from 'vitest'
import { groupTasksByStatus } from './riderTaskApi'
describe('groupTasksByStatus', () => {
it('groups assigned delivering and completed tasks', () => {
const grouped = groupTasksByStatus([
{ id: 1, status: 'ASSIGNED' },
{ id: 2, status: 'DELIVERING' },
{ id: 3, status: 'COMPLETED' }
])
expect(grouped.assigned).toHaveLength(1)
expect(grouped.delivering).toHaveLength(1)
expect(grouped.completed).toHaveLength(1)
})
})
- Step 2: Implement rider screens
Screens:
-
Login.
-
Today tasks with status tabs.
-
Task detail with user phone, address, order content, remark, and amount.
-
Action buttons: start delivery, delivered, exception, call user.
-
Upload delivered photo through
/api/files/upload. -
Income summary with completed count and estimated service fee.
-
Step 3: Verify
Run:
cd rider-h5
npm install
npm test
npm run build
Expected: tests pass and build succeeds.
- Step 4: Commit
git add rider-h5
git commit -m "feat: add rider h5 MVP"
Task 12: Integration, Seed Data, And Release Checks
Files:
-
Create:
backend/src/main/resources/db/migration/V2__seed_dev_data.sql -
Create:
docs/api/manual-test-checklist.md -
Create:
docs/deploy/local-runbook.md -
Modify:
README.md -
Step 1: Add development seed data
Create V2__seed_dev_data.sql with:
-
One tenant.
-
One community.
-
Module configs enabled for goods, express, group buy, secondhand, and notice.
-
Admin, merchant, rider, and resident users.
-
One merchant.
-
Six product categories.
-
At least ten products with SKUs.
-
One active group buy.
-
Two notices.
-
One banner.
-
Step 2: Add manual test checklist
Create docs/api/manual-test-checklist.md with these checks:
# Manual Test Checklist
- [ ] Admin can log in.
- [ ] Admin can create and publish a notice.
- [ ] Miniapp can list published notices.
- [ ] Admin can create product and SKU stock.
- [ ] Miniapp can submit goods order.
- [ ] Admin can confirm goods order and mark prepared.
- [ ] Admin can assign delivery order to rider.
- [ ] Rider can start delivery and upload delivered photo.
- [ ] Miniapp can confirm goods order completion.
- [ ] Miniapp can submit express pickup order.
- [ ] Admin can confirm express order and assign rider.
- [ ] Rider can mark express order delivered.
- [ ] Miniapp can submit group buy order before cutoff.
- [ ] Admin can review secondhand listing.
- [ ] Public secondhand list only shows approved listings.
- Step 3: Add local runbook
Create docs/deploy/local-runbook.md with commands:
docker compose -f deploy/docker-compose.dev.yml up -d
cd backend
mvn spring-boot:run
cd ..\admin-web
npm run dev
cd ..\rider-h5
npm run dev
Expected local URLs:
Backend API: http://localhost:8080
Knife4j docs: http://localhost:8080/doc.html
Admin web: http://localhost:5173
Rider H5: http://localhost:5174
MinIO console: http://localhost:9001
- Step 4: Run full verification
Run:
docker compose -f deploy/docker-compose.dev.yml up -d
cd backend
mvn test
cd ..\admin-web
npm test
npm run build
cd ..\miniapp
npm test
npm run build
cd ..\rider-h5
npm test
npm run build
git status --short
Expected:
-
Backend tests pass.
-
All frontend tests pass.
-
All builds succeed.
-
git status --shortonly shows intended changes before final commit. -
Step 5: Commit
git add backend/src/main/resources/db/migration/V2__seed_dev_data.sql docs README.md
git commit -m "docs: add release runbook and seed data"
Coverage Map
- PRD roles: covered by Tasks 3, 4, 7, 9, 10, 11.
- Product reservation: covered by Task 5 and Task 10.
- Offline payment status: covered by Tasks 5, 6, 9.
- Admin dispatch: covered by Task 7 and Task 9.
- Express pickup: covered by Task 6, Task 7, Task 10, Task 11.
- Group buy: covered by Task 8, Task 9, Task 10.
- Secondhand review: covered by Task 8, Task 9, Task 10.
- Notices and banners: covered by Task 4, Task 9, Task 10.
- SaaS-ready tenant/community fields: covered by Task 2 and enforced by Task 3.
- Rider H5: covered by Task 7 and Task 11.
Final Acceptance
The MVP is ready for one-community trial operation when:
- Backend full test suite passes.
- Admin web, miniapp, and rider H5 builds pass.
- 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.
- Miniapp only shows approved secondhand listings and published notices.
- All committed schema tables include SaaS-ready tenant/community scope where required.