diff --git a/docs/superpowers/plans/2026-07-07-linxiaobang-phase-one-implementation.md b/docs/superpowers/plans/2026-07-07-linxiaobang-phase-one-implementation.md
new file mode 100644
index 0000000..ce9b4bf
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-07-linxiaobang-phase-one-implementation.md
@@ -0,0 +1,2081 @@
+# 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.