# 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:
```text
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/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.
- 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:
```text
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.
```xml
4.0.0
com.linhelp
linhelp-backend
0.1.0-SNAPSHOT
linhelp-backend
17
3.3.2
3.5.7
1.38.0
4.5.0
8.5.11
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-data-redis
com.baomidou
mybatis-plus-spring-boot3-starter
${mybatis-plus.version}
cn.dev33
sa-token-spring-boot3-starter
${sa-token.version}
com.mysql
mysql-connector-j
runtime
org.flywaydb
flyway-core
org.flywaydb
flyway-mysql
com.github.xiaoymin
knife4j-openapi3-jakarta-spring-boot-starter
${knife4j.version}
io.minio
minio
${minio.version}
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
```
- [ ] **Step 2: Add Spring Boot entrypoint**
Create `backend/src/main/java/com/linhelp/LinHelpApplication.java`.
```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`.
```yaml
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`.
```yaml
spring:
profiles:
active: dev
application:
name: linhelp-backend
server:
port: 8080
knife4j:
enable: true
```
Create `backend/src/main/resources/application-dev.yml`.
```yaml
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`.
```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`.
```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`.
```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:
```powershell
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**
```powershell
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`.
```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:
```powershell
cd backend
mvn -Dtest=StatusTransitionTests test
```
Expected: FAIL because enum classes do not exist.
- [ ] **Step 3: Create common API wrappers**
Create `ApiResponse.java`.
```java
package com.linhelp.common.api;
public record ApiResponse(int code, String message, T data) {
public static ApiResponse ok(T data) {
return new ApiResponse<>(0, "ok", data);
}
public static ApiResponse ok() {
return new ApiResponse<>(0, "ok", null);
}
public static ApiResponse fail(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}
```
Create `PageResponse.java`.
```java
package com.linhelp.common.api;
import java.util.List;
public record PageResponse(long total, long pageNo, long pageSize, List records) {
}
```
- [ ] **Step 4: Create status enums with transition rules**
Create `GoodsOrderStatus.java`.
```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> 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`.
```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`.
```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:
```powershell
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**
```powershell
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`.
```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`.
```java
package com.linhelp.common.security;
public final class TenantContext {
private static final ThreadLocal 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:
```java
public record LoginRequest(String username, String password) {}
public record LoginResponse(String token, Long userId, Long tenantId, Long communityId, String roleCode) {}
```
Create `AuthController` endpoints:
```text
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:
```text
PLATFORM_ADMIN
COMMUNITY_ADMIN
MERCHANT
RIDER
```
- [ ] **Step 5: Verify**
Run:
```powershell
cd backend
mvn -Dtest=TenantContextTests test
mvn test
```
Expected: tenant tests and full backend tests pass.
- [ ] **Step 6: Commit**
```powershell
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**
```java
@Test
void settingOneAddressAsDefaultClearsOtherDefaultsForSameUser() {
addressService.create(userId, request("张三", "13800000000", "1栋", "101", true));
AddressResponse second = addressService.create(userId, request("张三", "13800000000", "1栋", "102", true));
List addresses = addressService.listMine(userId);
assertThat(addresses).filteredOn(AddressResponse::isDefault).singleElement()
.extracting(AddressResponse::id).isEqualTo(second.id());
}
```
- [ ] **Step 2: Implement user address APIs**
Create endpoints:
```text
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:
- `contactName` length 2-40.
- `phone` matches mainland mobile format or local service phone format.
- `building` and `room` are required.
- Users can only modify their own addresses in the current community.
- [ ] **Step 3: Implement notice and banner APIs**
Admin endpoints:
```text
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:
```text
GET /api/mini/notices
GET /api/mini/notices/{id}
GET /api/mini/banners
```
- [ ] **Step 4: Verify**
Run:
```powershell
cd backend
mvn -Dtest=UserAddressServiceTests,NoticeServiceTests test
mvn test
```
Expected: tests pass and OpenAPI shows address, notice, and banner groups.
- [ ] **Step 5: Commit**
```powershell
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**
```java
@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:
```text
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:
```text
GET /api/mini/product-categories
GET /api/mini/products
GET /api/mini/products/{id}
```
- [ ] **Step 3: Implement goods order creation**
Endpoint:
```text
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:
```text
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:
```powershell
cd backend
mvn -Dtest=ProductInventoryTests,GoodsOrderServiceTests test
mvn test
```
Expected: inventory, self-pickup, order status, and total amount tests pass.
- [ ] **Step 6: Commit**
```powershell
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**
```java
@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**
```java
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:
```text
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:
```text
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_CONFIRM` to `PENDING_PICKUP`.
- Fee adjustment writes an `express_log`.
- Canceled order records cancel reason.
- [ ] **Step 5: Verify**
Run:
```powershell
cd backend
mvn -Dtest=ExpressFeeCalculatorTests,ExpressOrderServiceTests test
mvn test
```
Expected: fee calculation and status tests pass.
- [ ] **Step 6: Commit**
```powershell
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**
```java
@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:
```text
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:
```text
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_PICKUP` to `PICKING_UP`, and later `PICKING_UP` to `DELIVERING` when package is marked picked.
- Delivered task stores `delivered_photo_url`.
- Exception task stores `exception_type` and `exception_note`.
- [ ] **Step 4: Verify**
Run:
```powershell
cd backend
mvn -Dtest=DeliveryAssignmentServiceTests,RiderTaskServiceTests test
mvn test
```
Expected: assignment, permission, and status sync tests pass.
- [ ] **Step 5: Commit**
```powershell
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**
```java
@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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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 `PUBLISHED` listings 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:
```powershell
cd backend
mvn -Dtest=GroupBuyServiceTests,SecondGoodsReviewTests test
mvn test
```
Expected: group buy cutoff, self-pickup, listing review, and visibility tests pass.
- [ ] **Step 6: Commit**
```powershell
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**
```ts
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:
```text
/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:
```powershell
cd admin-web
npm install
npm test
npm run build
```
Expected: Vitest passes and Vite build succeeds.
- [ ] **Step 5: Commit**
```powershell
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**
```ts
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:
```text
首页
团购
闲置
我的
```
Home shortcuts:
```text
商品预定
代取快递
今日团购
二手闲置
社区公告
我的订单
```
- [ ] **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:
```powershell
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**
```powershell
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**
```ts
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:
```powershell
cd rider-h5
npm install
npm test
npm run build
```
Expected: tests pass and build succeeds.
- [ ] **Step 4: Commit**
```powershell
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:
```markdown
# 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:
```powershell
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:
```text
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:
```powershell
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 --short` only shows intended changes before final commit.
- [ ] **Step 5: Commit**
```powershell
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.