Files
LinHelp/docs/superpowers/plans/2026-07-07-linxiaobang-phase-one-implementation.md
2026-07-07 14:46:20 +08:00

59 KiB

Linxiaobang Phase One Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build the phase-one "邻小帮" MVP: one-community operation with SaaS-ready data boundaries, offline payment, admin dispatch, resident miniapp, admin web, and rider H5.

Architecture: Use a backend-first implementation: define schema, status machines, API contracts, and tests before building the three clients. Keep the first release as a modular monolith with tenant/community fields on core tables, so later multi-community SaaS work can expand without rewriting order, delivery, and permission models.

Tech Stack: Spring Boot 3, Java 17, MyBatis Plus, Sa-Token, MySQL 8, Redis, Flyway, Knife4j/OpenAPI, MinIO-compatible object storage, Vue 3, TypeScript, Vite, Element Plus, uni-app/Vue 3 for WeChat miniapp, Vue 3 mobile H5 for riders.


Scope Check

The PRD spans four surfaces: backend API, admin web, resident miniapp, and rider H5. This plan keeps them in one MVP plan because the value is a single testable business loop, but it implements them in separately verifiable milestones:

  • Backend domain foundation and API contracts.
  • Admin web for management and dispatch.
  • Resident miniapp for orders and community content.
  • Rider H5 for fulfillment.
  • Integration and deployment.

If implementation time needs to be split across teams, Tasks 1-8 should be completed before client-heavy Tasks 9-11.

File Structure

Create this repository structure:

D:\codes\LinHelp
├── backend
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java\com\linhelp
│       │   │   ├── LinHelpApplication.java
│       │   │   ├── common
│       │   │   │   ├── api
│       │   │   │   ├── config
│       │   │   │   ├── enums
│       │   │   │   ├── exception
│       │   │   │   └── security
│       │   │   ├── community
│       │   │   ├── user
│       │   │   ├── merchant
│       │   │   ├── product
│       │   │   ├── order
│       │   │   ├── express
│       │   │   ├── groupbuy
│       │   │   ├── delivery
│       │   │   ├── secondhand
│       │   │   ├── notice
│       │   │   └── storage
│       │   └── resources
│       │       ├── application.yml
│       │       ├── application-dev.yml
│       │       └── db\migration
│       └── test\java\com\linhelp
├── admin-web
│   ├── package.json
│   ├── vite.config.ts
│   └── src
├── miniapp
│   ├── package.json
│   └── src
├── rider-h5
│   ├── package.json
│   ├── vite.config.ts
│   └── src
├── deploy
│   ├── docker-compose.dev.yml
│   └── nginx
├── docs
│   └── superpowers
└── README.md

Boundary rules:

  • backend/src/main/java/com/linhelp/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:

GoodsOrderStatus: PENDING_CONFIRM, PREPARING, PENDING_DELIVERY, PENDING_PICKUP, DELIVERING, COMPLETED, CANCELED
ExpressOrderStatus: PENDING_CONFIRM, PENDING_PICKUP, PICKING_UP, DELIVERING, DELIVERED, COMPLETED, CANCELED
GroupBuyOrderStatus: PENDING_CONFIRM, CONFIRMED, PENDING_DELIVERY, PENDING_PICKUP, DELIVERING, COMPLETED, CANCELED
SecondGoodsStatus: PENDING_REVIEW, PUBLISHED, REJECTED, OFF_SHELF, SOLD
DeliveryTaskStatus: ASSIGNED, ACCEPTED, DELIVERING, DELIVERED, EXCEPTION, COMPLETED, CANCELED
OfflinePayStatus: UNPAID, PAID, FREE, EXCEPTION
DeliveryMethod: IMMEDIATE, SCHEDULED, SELF_PICKUP

Task 1: Repository Scaffold And Local Runtime

Files:

  • Create: backend/pom.xml

  • Create: backend/src/main/java/com/linhelp/LinHelpApplication.java

  • Create: backend/src/main/resources/application.yml

  • Create: backend/src/main/resources/application-dev.yml

  • Create: backend/src/test/java/com/linhelp/LinHelpApplicationTests.java

  • Create: admin-web/package.json

  • Create: admin-web/vite.config.ts

  • Create: miniapp/package.json

  • Create: rider-h5/package.json

  • Create: rider-h5/vite.config.ts

  • Create: deploy/docker-compose.dev.yml

  • Create: .gitignore

  • Create: README.md

  • Step 1: Create backend Spring Boot skeleton

Add backend/pom.xml with Java 17, Spring Boot 3, MyBatis Plus, Sa-Token, MySQL, Redis, Flyway, Knife4j, MinIO, validation, and test dependencies.

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.linhelp</groupId>
    <artifactId>linhelp-backend</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <name>linhelp-backend</name>
    <properties>
        <java.version>17</java.version>
        <spring-boot.version>3.3.2</spring-boot.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
        <sa-token.version>1.38.0</sa-token.version>
        <knife4j.version>4.5.0</knife4j.version>
        <minio.version>8.5.11</minio.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot3-starter</artifactId>
            <version>${sa-token.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-mysql</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>${minio.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  • Step 2: Add Spring Boot entrypoint

Create backend/src/main/java/com/linhelp/LinHelpApplication.java.

package com.linhelp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LinHelpApplication {
    public static void main(String[] args) {
        SpringApplication.run(LinHelpApplication.class, args);
    }
}
  • Step 3: Add local infrastructure config

Create deploy/docker-compose.dev.yml.

services:
  mysql:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: linhelp
      MYSQL_USER: linhelp
      MYSQL_PASSWORD: linhelp
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
  redis:
    image: redis:7
    ports:
      - "6379:6379"
  minio:
    image: minio/minio:RELEASE.2025-04-22T22-12-26Z
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: linhelp
      MINIO_ROOT_PASSWORD: linhelp123
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - minio_data:/data
volumes:
  mysql_data:
  minio_data:
  • Step 4: Add backend YAML config

Create backend/src/main/resources/application.yml.

spring:
  profiles:
    active: dev
  application:
    name: linhelp-backend
server:
  port: 8080
knife4j:
  enable: true

Create backend/src/main/resources/application-dev.yml.

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/linhelp?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: linhelp
    password: linhelp
  data:
    redis:
      host: localhost
      port: 6379
  flyway:
    enabled: true
    locations: classpath:db/migration
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
sa-token:
  token-name: Authorization
  timeout: 2592000
linhelp:
  storage:
    endpoint: http://localhost:9000
    access-key: linhelp
    secret-key: linhelp123
    bucket: linhelp
  • Step 5: Add minimal frontend package files

Create admin-web/package.json.

{
  "name": "linhelp-admin-web",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite --host 0.0.0.0 --port 5173",
    "build": "vue-tsc -b && vite build",
    "test": "vitest run",
    "lint": "eslint ."
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.3.1",
    "axios": "^1.7.2",
    "element-plus": "^2.7.6",
    "pinia": "^2.1.7",
    "vue": "^3.4.31",
    "vue-router": "^4.4.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.5",
    "typescript": "^5.5.3",
    "vite": "^5.3.3",
    "vitest": "^2.0.2",
    "vue-tsc": "^2.0.26"
  }
}

Create miniapp/package.json.

{
  "name": "linhelp-miniapp",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev:mp-weixin": "uni -p mp-weixin",
    "build": "uni build -p mp-weixin",
    "test": "vitest run"
  },
  "dependencies": {
    "@dcloudio/uni-app": "^3.0.0",
    "@dcloudio/uni-mp-weixin": "^3.0.0",
    "axios": "^1.7.2",
    "pinia": "^2.1.7",
    "vue": "^3.4.31"
  },
  "devDependencies": {
    "@dcloudio/types": "^3.4.8",
    "@dcloudio/uni-cli-shared": "^3.0.0",
    "@dcloudio/vite-plugin-uni": "^3.0.0",
    "typescript": "^5.5.3",
    "vite": "^5.3.3",
    "vitest": "^2.0.2"
  }
}

Create rider-h5/package.json.

{
  "name": "linhelp-rider-h5",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite --host 0.0.0.0 --port 5174",
    "build": "vue-tsc -b && vite build",
    "test": "vitest run",
    "lint": "eslint ."
  },
  "dependencies": {
    "@vitejs/plugin-vue": "^5.0.5",
    "axios": "^1.7.2",
    "pinia": "^2.1.7",
    "vue": "^3.4.31",
    "vue-router": "^4.4.0"
  },
  "devDependencies": {
    "typescript": "^5.5.3",
    "vite": "^5.3.3",
    "vitest": "^2.0.2",
    "vue-tsc": "^2.0.26"
  }
}
  • Step 6: Verify scaffold

Run:

docker compose -f deploy/docker-compose.dev.yml up -d
cd backend
mvn test

Expected: MySQL, Redis, and MinIO containers start; mvn test passes or reports no failing tests.

  • Step 7: Commit
git add .gitignore README.md deploy backend admin-web miniapp rider-h5
git commit -m "chore: scaffold linhelp applications"

Task 2: Database Schema, Common API Shape, And Enums

Files:

  • Create: backend/src/main/resources/db/migration/V1__init_schema.sql

  • Create: backend/src/main/java/com/linhelp/common/api/ApiResponse.java

  • Create: backend/src/main/java/com/linhelp/common/api/PageResponse.java

  • Create: backend/src/main/java/com/linhelp/common/exception/BizException.java

  • Create: backend/src/main/java/com/linhelp/common/exception/GlobalExceptionHandler.java

  • Create: backend/src/main/java/com/linhelp/common/enums/*.java

  • Test: backend/src/test/java/com/linhelp/common/enums/StatusTransitionTests.java

  • Step 1: Write enum transition tests

Create StatusTransitionTests.java.

package com.linhelp.common.enums;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class StatusTransitionTests {
    @Test
    void goodsOrderCanMoveFromPendingConfirmToPreparing() {
        assertThat(GoodsOrderStatus.PENDING_CONFIRM.canMoveTo(GoodsOrderStatus.PREPARING)).isTrue();
    }

    @Test
    void goodsOrderCannotMoveFromCompletedToDelivering() {
        assertThat(GoodsOrderStatus.COMPLETED.canMoveTo(GoodsOrderStatus.DELIVERING)).isFalse();
    }

    @Test
    void expressOrderCanMoveThroughPickupFlow() {
        assertThat(ExpressOrderStatus.PENDING_CONFIRM.canMoveTo(ExpressOrderStatus.PENDING_PICKUP)).isTrue();
        assertThat(ExpressOrderStatus.PENDING_PICKUP.canMoveTo(ExpressOrderStatus.PICKING_UP)).isTrue();
        assertThat(ExpressOrderStatus.PICKING_UP.canMoveTo(ExpressOrderStatus.DELIVERING)).isTrue();
    }
}
  • Step 2: Run tests to verify compile failure

Run:

cd backend
mvn -Dtest=StatusTransitionTests test

Expected: FAIL because enum classes do not exist.

  • Step 3: Create common API wrappers

Create ApiResponse.java.

package com.linhelp.common.api;

public record ApiResponse<T>(int code, String message, T data) {
    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>(0, "ok", data);
    }

    public static ApiResponse<Void> ok() {
        return new ApiResponse<>(0, "ok", null);
    }

    public static ApiResponse<Void> fail(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }
}

Create PageResponse.java.

package com.linhelp.common.api;

import java.util.List;

public record PageResponse<T>(long total, long pageNo, long pageSize, List<T> records) {
}
  • Step 4: Create status enums with transition rules

Create GoodsOrderStatus.java.

package com.linhelp.common.enums;

import java.util.Map;
import java.util.Set;

public enum GoodsOrderStatus {
    PENDING_CONFIRM,
    PREPARING,
    PENDING_DELIVERY,
    PENDING_PICKUP,
    DELIVERING,
    COMPLETED,
    CANCELED;

    private static final Map<GoodsOrderStatus, Set<GoodsOrderStatus>> TRANSITIONS = Map.of(
            PENDING_CONFIRM, Set.of(PREPARING, CANCELED),
            PREPARING, Set.of(PENDING_DELIVERY, PENDING_PICKUP, CANCELED),
            PENDING_DELIVERY, Set.of(DELIVERING, CANCELED),
            PENDING_PICKUP, Set.of(COMPLETED, CANCELED),
            DELIVERING, Set.of(COMPLETED, CANCELED)
    );

    public boolean canMoveTo(GoodsOrderStatus target) {
        return TRANSITIONS.getOrDefault(this, Set.of()).contains(target);
    }
}

Create the remaining enum files with matching names from "Shared Domain Vocabulary". Give ExpressOrderStatus the same canMoveTo method and a transition map for PENDING_CONFIRM -> PENDING_PICKUP -> PICKING_UP -> DELIVERING -> DELIVERED -> COMPLETED, with cancellation allowed before completion.

  • Step 5: Create initial schema

Create backend/src/main/resources/db/migration/V1__init_schema.sql.

CREATE TABLE tenant (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(80) NOT NULL,
  status VARCHAR(24) NOT NULL DEFAULT 'ENABLED',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0
);

CREATE TABLE community (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  name VARCHAR(80) NOT NULL,
  address VARCHAR(255) NOT NULL,
  contact_name VARCHAR(40),
  contact_phone VARCHAR(32),
  status VARCHAR(24) NOT NULL DEFAULT 'ENABLED',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_community_tenant (tenant_id)
);

CREATE TABLE community_module_config (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  module_code VARCHAR(40) NOT NULL,
  enabled TINYINT NOT NULL DEFAULT 1,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_community_module (community_id, module_code)
);

CREATE TABLE app_user (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  openid VARCHAR(80),
  nickname VARCHAR(80),
  avatar_url VARCHAR(255),
  phone VARCHAR(32),
  status VARCHAR(24) NOT NULL DEFAULT 'ENABLED',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_app_user_openid (openid),
  INDEX idx_app_user_community (community_id)
);

CREATE TABLE user_address (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  contact_name VARCHAR(40) NOT NULL,
  phone VARCHAR(32) NOT NULL,
  building VARCHAR(80) NOT NULL,
  room VARCHAR(80) NOT NULL,
  detail VARCHAR(255),
  is_default TINYINT NOT NULL DEFAULT 0,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_user_address_user (user_id)
);

CREATE TABLE sys_role (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  code VARCHAR(40) NOT NULL UNIQUE,
  name VARCHAR(80) NOT NULL
);

CREATE TABLE user_role (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  role_code VARCHAR(40) NOT NULL,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT,
  merchant_id BIGINT,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_user_role_scope (user_id, role_code, tenant_id, community_id, merchant_id)
);

CREATE TABLE merchant (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  name VARCHAR(100) NOT NULL,
  merchant_type VARCHAR(40) NOT NULL,
  contact_name VARCHAR(40),
  contact_phone VARCHAR(32),
  status VARCHAR(24) NOT NULL DEFAULT 'ENABLED',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_merchant_community (community_id)
);

CREATE TABLE product_category (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  name VARCHAR(80) NOT NULL,
  sort_no INT NOT NULL DEFAULT 0,
  enabled TINYINT NOT NULL DEFAULT 1,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0
);

CREATE TABLE product (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  merchant_id BIGINT NOT NULL,
  category_id BIGINT NOT NULL,
  name VARCHAR(120) NOT NULL,
  cover_url VARCHAR(255),
  description TEXT,
  unit_name VARCHAR(20) NOT NULL DEFAULT '件',
  status VARCHAR(24) NOT NULL DEFAULT 'OFF_SHELF',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_product_category (category_id),
  INDEX idx_product_merchant (merchant_id)
);

CREATE TABLE product_sku (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  product_id BIGINT NOT NULL,
  sku_name VARCHAR(80) NOT NULL,
  price_cent INT NOT NULL,
  origin_price_cent INT,
  stock INT NOT NULL DEFAULT 0,
  enabled TINYINT NOT NULL DEFAULT 1,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_product_sku_product (product_id)
);

CREATE TABLE goods_order (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_no VARCHAR(40) NOT NULL UNIQUE,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  merchant_id BIGINT NOT NULL,
  address_id BIGINT,
  delivery_method VARCHAR(24) NOT NULL,
  scheduled_time DATETIME,
  status VARCHAR(32) NOT NULL,
  offline_pay_status VARCHAR(24) NOT NULL DEFAULT 'UNPAID',
  total_amount_cent INT NOT NULL,
  remark VARCHAR(255),
  cancel_reason VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_goods_order_user (user_id),
  INDEX idx_goods_order_status (community_id, status)
);

CREATE TABLE goods_order_item (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  order_id BIGINT NOT NULL,
  product_id BIGINT NOT NULL,
  sku_id BIGINT NOT NULL,
  product_name VARCHAR(120) NOT NULL,
  sku_name VARCHAR(80) NOT NULL,
  quantity INT NOT NULL,
  price_cent INT NOT NULL,
  amount_cent INT NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_goods_order_item_order (order_id)
);

CREATE TABLE express_order (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_no VARCHAR(40) NOT NULL UNIQUE,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  address_id BIGINT NOT NULL,
  express_company VARCHAR(80) NOT NULL,
  pickup_code VARCHAR(120) NOT NULL,
  pickup_address VARCHAR(255) NOT NULL,
  package_count INT NOT NULL,
  service_fee_cent INT NOT NULL,
  phone VARCHAR(32) NOT NULL,
  image_url VARCHAR(255),
  status VARCHAR(32) NOT NULL,
  offline_pay_status VARCHAR(24) NOT NULL DEFAULT 'UNPAID',
  remark VARCHAR(255),
  cancel_reason VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_express_order_status (community_id, status)
);

CREATE TABLE delivery_user (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  name VARCHAR(40) NOT NULL,
  phone VARCHAR(32) NOT NULL,
  status VARCHAR(24) NOT NULL DEFAULT 'ENABLED',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0
);

CREATE TABLE delivery_order (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  biz_type VARCHAR(32) NOT NULL,
  biz_order_id BIGINT NOT NULL,
  delivery_user_id BIGINT NOT NULL,
  status VARCHAR(32) NOT NULL,
  delivered_photo_url VARCHAR(255),
  exception_type VARCHAR(64),
  exception_note VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  UNIQUE KEY uk_delivery_biz_order (biz_type, biz_order_id),
  INDEX idx_delivery_user_status (delivery_user_id, status)
);

Append these SQL table definitions to V1__init_schema.sql.

CREATE TABLE stock_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  product_id BIGINT NOT NULL,
  sku_id BIGINT NOT NULL,
  change_quantity INT NOT NULL,
  before_stock INT NOT NULL,
  after_stock INT NOT NULL,
  biz_type VARCHAR(32) NOT NULL,
  biz_order_id BIGINT,
  note VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_stock_log_sku (sku_id)
);

CREATE TABLE order_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  biz_type VARCHAR(32) NOT NULL,
  biz_order_id BIGINT NOT NULL,
  from_status VARCHAR(32),
  to_status VARCHAR(32),
  action VARCHAR(64) NOT NULL,
  operator_id BIGINT,
  note VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_order_log_biz (biz_type, biz_order_id)
);

CREATE TABLE express_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  express_order_id BIGINT NOT NULL,
  from_status VARCHAR(32),
  to_status VARCHAR(32),
  action VARCHAR(64) NOT NULL,
  operator_id BIGINT,
  note VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_express_log_order (express_order_id)
);

CREATE TABLE group_buy (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  merchant_id BIGINT NOT NULL,
  title VARCHAR(120) NOT NULL,
  cover_url VARCHAR(255),
  description TEXT,
  price_cent INT NOT NULL,
  origin_price_cent INT,
  stock INT NOT NULL,
  deadline_at DATETIME NOT NULL,
  expected_delivery_at DATETIME,
  status VARCHAR(32) NOT NULL DEFAULT 'NOT_STARTED',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_group_buy_status (community_id, status)
);

CREATE TABLE group_buy_order (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_no VARCHAR(40) NOT NULL UNIQUE,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  group_buy_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  address_id BIGINT,
  delivery_method VARCHAR(24) NOT NULL,
  quantity INT NOT NULL,
  amount_cent INT NOT NULL,
  status VARCHAR(32) NOT NULL,
  offline_pay_status VARCHAR(24) NOT NULL DEFAULT 'UNPAID',
  remark VARCHAR(255),
  cancel_reason VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_group_buy_order_activity (group_buy_id),
  INDEX idx_group_buy_order_user (user_id)
);

CREATE TABLE delivery_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  delivery_order_id BIGINT NOT NULL,
  from_status VARCHAR(32),
  to_status VARCHAR(32),
  action VARCHAR(64) NOT NULL,
  operator_id BIGINT,
  note VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_delivery_log_order (delivery_order_id)
);

CREATE TABLE second_goods (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  title VARCHAR(120) NOT NULL,
  price_cent INT NOT NULL,
  description TEXT,
  category VARCHAR(40) NOT NULL,
  contact_phone VARCHAR(32) NOT NULL,
  trade_method VARCHAR(32) NOT NULL,
  status VARCHAR(32) NOT NULL,
  reject_reason VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_second_goods_status (community_id, status),
  INDEX idx_second_goods_user (user_id)
);

CREATE TABLE second_goods_img (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  second_goods_id BIGINT NOT NULL,
  image_url VARCHAR(255) NOT NULL,
  sort_no INT NOT NULL DEFAULT 0,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_second_goods_img_goods (second_goods_id)
);

CREATE TABLE second_goods_comment (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  second_goods_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  content VARCHAR(500) NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_second_goods_comment_goods (second_goods_id)
);

CREATE TABLE notice (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  title VARCHAR(120) NOT NULL,
  content TEXT NOT NULL,
  category VARCHAR(40) NOT NULL,
  pinned TINYINT NOT NULL DEFAULT 0,
  status VARCHAR(24) NOT NULL DEFAULT 'DRAFT',
  published_at DATETIME,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_notice_status (community_id, status, pinned)
);

CREATE TABLE banner (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  title VARCHAR(120) NOT NULL,
  image_url VARCHAR(255) NOT NULL,
  link_type VARCHAR(32),
  link_value VARCHAR(255),
  sort_no INT NOT NULL DEFAULT 0,
  enabled TINYINT NOT NULL DEFAULT 1,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_banner_community (community_id, enabled)
);

CREATE TABLE feedback (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  biz_type VARCHAR(32),
  biz_order_id BIGINT,
  content VARCHAR(500) NOT NULL,
  status VARCHAR(24) NOT NULL DEFAULT 'OPEN',
  reply VARCHAR(500),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted TINYINT NOT NULL DEFAULT 0,
  INDEX idx_feedback_status (community_id, status)
);

CREATE TABLE payment_record (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  biz_type VARCHAR(32) NOT NULL,
  biz_order_id BIGINT NOT NULL,
  amount_cent INT NOT NULL,
  pay_method VARCHAR(32) NOT NULL DEFAULT 'OFFLINE',
  pay_status VARCHAR(24) NOT NULL,
  paid_at DATETIME,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_payment_biz (biz_type, biz_order_id)
);

CREATE TABLE refund_record (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  community_id BIGINT NOT NULL,
  payment_record_id BIGINT,
  biz_type VARCHAR(32) NOT NULL,
  biz_order_id BIGINT NOT NULL,
  amount_cent INT NOT NULL,
  refund_status VARCHAR(24) NOT NULL,
  reason VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_refund_biz (biz_type, biz_order_id)
);
  • Step 6: Run migration and tests

Run:

docker compose -f deploy/docker-compose.dev.yml up -d
cd backend
mvn test

Expected: Flyway applies V1__init_schema.sql; enum tests pass.

  • Step 7: Commit
git add backend/src/main/resources/db/migration backend/src/main/java/com/linhelp/common backend/src/test/java/com/linhelp/common
git commit -m "feat: add core schema and status enums"

Task 3: Authentication, Tenant Context, And Permissions

Files:

  • Create: backend/src/main/java/com/linhelp/common/security/TenantContext.java

  • Create: backend/src/main/java/com/linhelp/common/security/CurrentUser.java

  • Create: backend/src/main/java/com/linhelp/common/security/AuthController.java

  • Create: backend/src/main/java/com/linhelp/common/security/AuthService.java

  • Create: backend/src/main/java/com/linhelp/common/security/RequireRole.java

  • Create: backend/src/main/java/com/linhelp/common/config/SaTokenConfig.java

  • Test: backend/src/test/java/com/linhelp/common/security/TenantContextTests.java

  • Step 1: Write tenant context tests

Create TenantContextTests.java.

package com.linhelp.common.security;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class TenantContextTests {
    @AfterEach
    void clear() {
        TenantContext.clear();
    }

    @Test
    void storesTenantAndCommunityForCurrentRequestThread() {
        TenantContext.set(1L, 10L);

        assertThat(TenantContext.tenantId()).isEqualTo(1L);
        assertThat(TenantContext.communityId()).isEqualTo(10L);
    }
}
  • Step 2: Implement tenant context

Create TenantContext.java.

package com.linhelp.common.security;

public final class TenantContext {
    private static final ThreadLocal<Scope> SCOPE = new ThreadLocal<>();

    private TenantContext() {
    }

    public static void set(Long tenantId, Long communityId) {
        SCOPE.set(new Scope(tenantId, communityId));
    }

    public static Long tenantId() {
        return SCOPE.get() == null ? null : SCOPE.get().tenantId();
    }

    public static Long communityId() {
        return SCOPE.get() == null ? null : SCOPE.get().communityId();
    }

    public static void clear() {
        SCOPE.remove();
    }

    private record Scope(Long tenantId, Long communityId) {
    }
}
  • Step 3: Implement simple admin login

Use Sa-Token for session management. First implementation supports backend account login for admin, merchant, and rider users. WeChat miniapp login can initially accept a dev openid in local mode and be replaced with jscode2session in a later integration task.

Create request/response DTOs:

public record LoginRequest(String username, String password) {}
public record LoginResponse(String token, Long userId, Long tenantId, Long communityId, String roleCode) {}

Create AuthController endpoints:

POST /api/auth/admin-login
POST /api/auth/dev-miniapp-login
POST /api/auth/logout
GET  /api/auth/me
  • Step 4: Add role guard

Create RequireRole.java as an annotation and add an interceptor or Sa-Token route check so admin endpoints require one of:

PLATFORM_ADMIN
COMMUNITY_ADMIN
MERCHANT
RIDER
  • Step 5: Verify

Run:

cd backend
mvn -Dtest=TenantContextTests test
mvn test

Expected: tenant tests and full backend tests pass.

  • Step 6: Commit
git add backend/src/main/java/com/linhelp/common/security backend/src/main/java/com/linhelp/common/config backend/src/test/java/com/linhelp/common/security
git commit -m "feat: add authentication and tenant context"

Task 4: Community, Notices, Banners, Users, And Addresses

Files:

  • Create: backend/src/main/java/com/linhelp/community/*

  • Create: backend/src/main/java/com/linhelp/notice/*

  • Create: backend/src/main/java/com/linhelp/user/*

  • Test: backend/src/test/java/com/linhelp/user/UserAddressServiceTests.java

  • Test: backend/src/test/java/com/linhelp/notice/NoticeServiceTests.java

  • Step 1: Write address default behavior test

@Test
void settingOneAddressAsDefaultClearsOtherDefaultsForSameUser() {
    addressService.create(userId, request("张三", "13800000000", "1栋", "101", true));
    AddressResponse second = addressService.create(userId, request("张三", "13800000000", "1栋", "102", true));

    List<AddressResponse> addresses = addressService.listMine(userId);

    assertThat(addresses).filteredOn(AddressResponse::isDefault).singleElement()
            .extracting(AddressResponse::id).isEqualTo(second.id());
}
  • Step 2: Implement user address APIs

Create endpoints:

GET    /api/mini/addresses
POST   /api/mini/addresses
PUT    /api/mini/addresses/{id}
DELETE /api/mini/addresses/{id}
PUT    /api/mini/addresses/{id}/default

Validation:

  • 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:

GET    /api/admin/notices
POST   /api/admin/notices
PUT    /api/admin/notices/{id}
PUT    /api/admin/notices/{id}/publish
PUT    /api/admin/notices/{id}/offline
DELETE /api/admin/notices/{id}
GET    /api/admin/banners
POST   /api/admin/banners
PUT    /api/admin/banners/{id}
DELETE /api/admin/banners/{id}

Miniapp endpoints:

GET /api/mini/notices
GET /api/mini/notices/{id}
GET /api/mini/banners
  • Step 4: Verify

Run:

cd backend
mvn -Dtest=UserAddressServiceTests,NoticeServiceTests test
mvn test

Expected: tests pass and OpenAPI shows address, notice, and banner groups.

  • Step 5: Commit
git add backend/src/main/java/com/linhelp/community backend/src/main/java/com/linhelp/notice backend/src/main/java/com/linhelp/user backend/src/test/java/com/linhelp/user backend/src/test/java/com/linhelp/notice
git commit -m "feat: add community content and address APIs"

Task 5: Product Catalog, SKU Inventory, And Goods Orders

Files:

  • Create: backend/src/main/java/com/linhelp/merchant/*

  • Create: backend/src/main/java/com/linhelp/product/*

  • Create: backend/src/main/java/com/linhelp/order/*

  • Test: backend/src/test/java/com/linhelp/order/GoodsOrderServiceTests.java

  • Test: backend/src/test/java/com/linhelp/product/ProductInventoryTests.java

  • Step 1: Write inventory and order tests

@Test
void cannotCreateGoodsOrderWhenSkuStockIsInsufficient() {
    CreateGoodsOrderRequest request = new CreateGoodsOrderRequest(
            addressId,
            DeliveryMethod.IMMEDIATE,
            null,
            List.of(new CreateGoodsOrderItemRequest(productId, skuId, 99)),
            "请尽快送达"
    );

    assertThatThrownBy(() -> goodsOrderService.create(userId, request))
            .hasMessageContaining("库存不足");
}

@Test
void selfPickupOrderMovesToPendingPickupAfterPreparing() {
    GoodsOrderResponse order = goodsOrderService.create(userId, selfPickupRequest());

    goodsOrderService.confirm(order.id());
    goodsOrderService.markPrepared(order.id());

    assertThat(goodsOrderService.detail(order.id()).status()).isEqualTo(GoodsOrderStatus.PENDING_PICKUP);
}
  • Step 2: Implement merchant and product management APIs

Admin endpoints:

GET    /api/admin/merchants
POST   /api/admin/merchants
PUT    /api/admin/merchants/{id}
PUT    /api/admin/merchants/{id}/enable
PUT    /api/admin/merchants/{id}/disable
GET    /api/admin/product-categories
POST   /api/admin/product-categories
PUT    /api/admin/product-categories/{id}
DELETE /api/admin/product-categories/{id}
GET    /api/admin/products
POST   /api/admin/products
PUT    /api/admin/products/{id}
PUT    /api/admin/products/{id}/on-shelf
PUT    /api/admin/products/{id}/off-shelf
POST   /api/admin/products/{id}/skus
PUT    /api/admin/product-skus/{id}

Miniapp endpoints:

GET /api/mini/product-categories
GET /api/mini/products
GET /api/mini/products/{id}
  • Step 3: Implement goods order creation

Endpoint:

POST /api/mini/goods-orders
GET  /api/mini/goods-orders
GET  /api/mini/goods-orders/{id}
PUT  /api/mini/goods-orders/{id}/cancel
PUT  /api/mini/goods-orders/{id}/complete

Creation rules:

  • Offline payment status starts as UNPAID.

  • Status starts as PENDING_CONFIRM.

  • Total amount is calculated from current SKU prices.

  • Stock is deducted at creation and recorded in stock_log.

  • Scheduled delivery requires scheduledTime.

  • Self pickup does not create a delivery task.

  • Step 4: Implement admin goods order operations

Endpoints:

GET /api/admin/goods-orders
GET /api/admin/goods-orders/{id}
PUT /api/admin/goods-orders/{id}/confirm
PUT /api/admin/goods-orders/{id}/prepared
PUT /api/admin/goods-orders/{id}/cancel
PUT /api/admin/goods-orders/{id}/offline-pay-status
  • Step 5: Verify

Run:

cd backend
mvn -Dtest=ProductInventoryTests,GoodsOrderServiceTests test
mvn test

Expected: inventory, self-pickup, order status, and total amount tests pass.

  • Step 6: Commit
git add backend/src/main/java/com/linhelp/merchant backend/src/main/java/com/linhelp/product backend/src/main/java/com/linhelp/order backend/src/test/java/com/linhelp/product backend/src/test/java/com/linhelp/order
git commit -m "feat: add product catalog and goods orders"

Task 6: Express Pickup Orders

Files:

  • Create: backend/src/main/java/com/linhelp/express/*

  • Test: backend/src/test/java/com/linhelp/express/ExpressFeeCalculatorTests.java

  • Test: backend/src/test/java/com/linhelp/express/ExpressOrderServiceTests.java

  • Step 1: Write express fee tests

@Test
void firstPackageCostsThreeYuan() {
    assertThat(ExpressFeeCalculator.calculateCent(1)).isEqualTo(300);
}

@Test
void extraPackagesCostOneYuanEach() {
    assertThat(ExpressFeeCalculator.calculateCent(3)).isEqualTo(500);
}

@Test
void packageCountMustBePositive() {
    assertThatThrownBy(() -> ExpressFeeCalculator.calculateCent(0))
            .hasMessageContaining("件数必须大于0");
}
  • Step 2: Implement fee calculator
package com.linhelp.express;

public final class ExpressFeeCalculator {
    private ExpressFeeCalculator() {
    }

    public static int calculateCent(int packageCount) {
        if (packageCount <= 0) {
            throw new IllegalArgumentException("件数必须大于0");
        }
        return 300 + Math.max(0, packageCount - 1) * 100;
    }
}
  • Step 3: Implement miniapp express APIs

Endpoints:

POST /api/mini/express-orders
GET  /api/mini/express-orders
GET  /api/mini/express-orders/{id}
PUT  /api/mini/express-orders/{id}/cancel
PUT  /api/mini/express-orders/{id}/complete

Validation:

  • Express company is required.

  • Pickup code is required.

  • Pickup address is required.

  • Address ID must belong to current user.

  • Package count must be greater than zero.

  • Step 4: Implement admin express APIs

Endpoints:

GET /api/admin/express-orders
GET /api/admin/express-orders/{id}
PUT /api/admin/express-orders/{id}/confirm
PUT /api/admin/express-orders/{id}/adjust-fee
PUT /api/admin/express-orders/{id}/cancel
PUT /api/admin/express-orders/{id}/offline-pay-status

Rules:

  • Confirmed order moves from PENDING_CONFIRM to PENDING_PICKUP.

  • Fee adjustment writes an express_log.

  • Canceled order records cancel reason.

  • Step 5: Verify

Run:

cd backend
mvn -Dtest=ExpressFeeCalculatorTests,ExpressOrderServiceTests test
mvn test

Expected: fee calculation and status tests pass.

  • Step 6: Commit
git add backend/src/main/java/com/linhelp/express backend/src/test/java/com/linhelp/express
git commit -m "feat: add express pickup orders"

Task 7: Delivery Assignment And Rider APIs

Files:

  • Create: backend/src/main/java/com/linhelp/delivery/*

  • Test: backend/src/test/java/com/linhelp/delivery/DeliveryAssignmentServiceTests.java

  • Test: backend/src/test/java/com/linhelp/delivery/RiderTaskServiceTests.java

  • Step 1: Write assignment tests

@Test
void adminCanAssignPendingDeliveryGoodsOrderToRider() {
    GoodsOrderResponse order = preparedDeliveryOrder();
    DeliveryOrderResponse task = deliveryAssignmentService.assignGoodsOrder(order.id(), riderId);

    assertThat(task.bizType()).isEqualTo("GOODS_ORDER");
    assertThat(task.status()).isEqualTo(DeliveryTaskStatus.ASSIGNED);
}

@Test
void cannotAssignSelfPickupGoodsOrder() {
    GoodsOrderResponse order = preparedSelfPickupOrder();

    assertThatThrownBy(() -> deliveryAssignmentService.assignGoodsOrder(order.id(), riderId))
            .hasMessageContaining("自提订单无需派单");
}
  • Step 2: Implement delivery admin APIs

Endpoints:

GET  /api/admin/delivery-users
POST /api/admin/delivery-users
PUT  /api/admin/delivery-users/{id}
PUT  /api/admin/delivery-users/{id}/enable
PUT  /api/admin/delivery-users/{id}/disable
POST /api/admin/delivery-orders/assign-goods-order
POST /api/admin/delivery-orders/assign-express-order
POST /api/admin/delivery-orders/assign-group-buy-order
GET  /api/admin/delivery-orders
GET  /api/admin/delivery-orders/{id}
  • Step 3: Implement rider H5 APIs

Endpoints:

GET /api/rider/tasks
GET /api/rider/tasks/{id}
PUT /api/rider/tasks/{id}/start
PUT /api/rider/tasks/{id}/delivered
PUT /api/rider/tasks/{id}/exception
GET /api/rider/income-summary

Rules:

  • Rider only sees assigned tasks for their own delivery_user_id.

  • Starting a goods order moves the linked goods order to DELIVERING.

  • Starting an express order moves PENDING_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:

cd backend
mvn -Dtest=DeliveryAssignmentServiceTests,RiderTaskServiceTests test
mvn test

Expected: assignment, permission, and status sync tests pass.

  • Step 5: Commit
git add backend/src/main/java/com/linhelp/delivery backend/src/test/java/com/linhelp/delivery
git commit -m "feat: add delivery assignment and rider APIs"

Task 8: Group Buy, Secondhand, Storage, And Feedback

Files:

  • Create: backend/src/main/java/com/linhelp/groupbuy/*

  • Create: backend/src/main/java/com/linhelp/secondhand/*

  • Create: backend/src/main/java/com/linhelp/storage/*

  • Create: backend/src/main/java/com/linhelp/feedback/*

  • Test: backend/src/test/java/com/linhelp/groupbuy/GroupBuyServiceTests.java

  • Test: backend/src/test/java/com/linhelp/secondhand/SecondGoodsReviewTests.java

  • Step 1: Write group buy tests

@Test
void cannotOrderAfterGroupBuyDeadline() {
    GroupBuyResponse groupBuy = groupBuyFixture.closedGroupBuy();

    assertThatThrownBy(() -> groupBuyOrderService.create(userId, requestFor(groupBuy.id())))
            .hasMessageContaining("团购已截单");
}

@Test
void groupBuyOrderCanBePreparedForSelfPickup() {
    GroupBuyOrderResponse order = groupBuyOrderService.create(userId, selfPickupGroupBuyRequest());
    groupBuyOrderService.confirm(order.id());
    groupBuyOrderService.markReady(order.id());

    assertThat(groupBuyOrderService.detail(order.id()).status()).isEqualTo(GroupBuyOrderStatus.PENDING_PICKUP);
}
  • Step 2: Implement storage APIs

Endpoint:

POST /api/files/upload

Rules:

  • Accept image MIME types only for phase one.

  • Store object with path prefix tenant/{tenantId}/community/{communityId}/yyyy/MM/dd/.

  • Return public or signed URL according to MinIO config.

  • Step 3: Implement group buy APIs

Admin endpoints:

GET    /api/admin/group-buys
POST   /api/admin/group-buys
PUT    /api/admin/group-buys/{id}
PUT    /api/admin/group-buys/{id}/start
PUT    /api/admin/group-buys/{id}/close
PUT    /api/admin/group-buys/{id}/complete
GET    /api/admin/group-buys/{id}/orders

Miniapp endpoints:

GET  /api/mini/group-buys
GET  /api/mini/group-buys/{id}
POST /api/mini/group-buy-orders
GET  /api/mini/group-buy-orders
GET  /api/mini/group-buy-orders/{id}
PUT  /api/mini/group-buy-orders/{id}/cancel
PUT  /api/mini/group-buy-orders/{id}/complete
  • Step 4: Implement secondhand APIs

Admin endpoints:

GET /api/admin/second-goods
PUT /api/admin/second-goods/{id}/approve
PUT /api/admin/second-goods/{id}/reject
PUT /api/admin/second-goods/{id}/off-shelf

Miniapp endpoints:

GET  /api/mini/second-goods
GET  /api/mini/second-goods/{id}
POST /api/mini/second-goods
PUT  /api/mini/second-goods/{id}/off-shelf
PUT  /api/mini/second-goods/{id}/sold
POST /api/mini/second-goods/{id}/comments

Rules:

  • New listing starts as PENDING_REVIEW.

  • Only 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:

cd backend
mvn -Dtest=GroupBuyServiceTests,SecondGoodsReviewTests test
mvn test

Expected: group buy cutoff, self-pickup, listing review, and visibility tests pass.

  • Step 6: Commit
git add backend/src/main/java/com/linhelp/groupbuy backend/src/main/java/com/linhelp/secondhand backend/src/main/java/com/linhelp/storage backend/src/main/java/com/linhelp/feedback backend/src/test/java/com/linhelp/groupbuy backend/src/test/java/com/linhelp/secondhand
git commit -m "feat: add group buy secondhand and file storage"

Task 9: Admin Web MVP

Files:

  • Create: admin-web/src/main.ts

  • Create: admin-web/src/App.vue

  • Create: admin-web/src/router/index.ts

  • Create: admin-web/src/api/http.ts

  • Create: admin-web/src/api/*.ts

  • Create: admin-web/src/layout/AdminLayout.vue

  • Create: admin-web/src/views/LoginView.vue

  • Create: admin-web/src/views/DashboardView.vue

  • Create: admin-web/src/views/ProductListView.vue

  • Create: admin-web/src/views/GoodsOrderListView.vue

  • Create: admin-web/src/views/ExpressOrderListView.vue

  • Create: admin-web/src/views/DeliveryDispatchView.vue

  • Create: admin-web/src/views/GroupBuyListView.vue

  • Create: admin-web/src/views/SecondGoodsReviewView.vue

  • Create: admin-web/src/views/NoticeListView.vue

  • Test: admin-web/src/api/orderApi.test.ts

  • Step 1: Create typed API client tests

import { describe, expect, it } from 'vitest'
import { buildGoodsOrderQuery } from './orderApi'

describe('buildGoodsOrderQuery', () => {
  it('keeps status and date range filters stable', () => {
    expect(buildGoodsOrderQuery({
      status: 'PENDING_DELIVERY',
      startDate: '2026-07-01',
      endDate: '2026-07-07'
    })).toEqual({
      status: 'PENDING_DELIVERY',
      startDate: '2026-07-01',
      endDate: '2026-07-07'
    })
  })
})
  • Step 2: Implement admin shell

Routes:

/login
/
/products
/goods-orders
/express-orders
/dispatch
/group-buys
/second-goods
/notices

Layout:

  • Left navigation.

  • Top bar with current community and logout.

  • Main content uses Element Plus table/form patterns.

  • Step 3: Implement management screens

Screen minimums:

  • Dashboard: today orders, receivable amount, pending confirmation, pending delivery, express count.

  • Product list: search, create, edit, SKU stock, on-shelf/off-shelf.

  • Goods order list: filter, detail drawer, confirm, prepared, cancel, offline payment status.

  • Express order list: filter, detail drawer, confirm, adjust fee, cancel, offline payment status.

  • Dispatch: tabs for goods, express, group buy; choose rider; assign.

  • Group buy list: create, edit, start, close, complete, order summary.

  • Second goods review: approve, reject with reason, off-shelf.

  • Notice list: create, edit, publish, offline, delete.

  • Step 4: Verify

Run:

cd admin-web
npm install
npm test
npm run build

Expected: Vitest passes and Vite build succeeds.

  • Step 5: Commit
git add admin-web
git commit -m "feat: add admin web MVP"

Task 10: Resident Miniapp MVP

Files:

  • Create: miniapp/src/main.ts

  • Create: miniapp/src/App.vue

  • Create: miniapp/src/pages.json

  • Create: miniapp/src/api/*.ts

  • Create: miniapp/src/pages/home/index.vue

  • Create: miniapp/src/pages/products/index.vue

  • Create: miniapp/src/pages/products/detail.vue

  • Create: miniapp/src/pages/orders/index.vue

  • Create: miniapp/src/pages/orders/detail.vue

  • Create: miniapp/src/pages/express/create.vue

  • Create: miniapp/src/pages/group-buy/index.vue

  • Create: miniapp/src/pages/group-buy/detail.vue

  • Create: miniapp/src/pages/second-hand/index.vue

  • Create: miniapp/src/pages/second-hand/create.vue

  • Create: miniapp/src/pages/notices/index.vue

  • Create: miniapp/src/pages/profile/index.vue

  • Create: miniapp/src/pages/address/index.vue

  • Test: miniapp/src/api/expressApi.test.ts

  • Step 1: Create express fee UI helper test

import { describe, expect, it } from 'vitest'
import { calculateExpressFeeCent } from './expressApi'

describe('calculateExpressFeeCent', () => {
  it('matches backend express fee rule', () => {
    expect(calculateExpressFeeCent(1)).toBe(300)
    expect(calculateExpressFeeCent(2)).toBe(400)
    expect(calculateExpressFeeCent(4)).toBe(600)
  })
})
  • Step 2: Implement miniapp navigation

Tabs:

首页
团购
闲置
我的

Home shortcuts:

商品预定
代取快递
今日团购
二手闲置
社区公告
我的订单
  • Step 3: Implement resident flows

Minimum flows:

  • Browse home banners, notices, hot products, group buys, and secondhand listings.

  • Browse product categories and product detail.

  • Submit goods order with delivery method, address, scheduled time, and remark.

  • Submit express pickup order with package count and auto fee display.

  • Submit group buy order.

  • Publish secondhand listing and show pending review state.

  • Manage addresses and default address.

  • View order lists by type and detail pages.

  • Step 4: Verify

Run:

cd miniapp
npm install
npm test
npm run build

Expected: API helper tests pass and npm run build produces the WeChat miniapp output under miniapp/dist/build/mp-weixin.

  • Step 5: Commit
git add miniapp
git commit -m "feat: add resident miniapp MVP"

Task 11: Rider H5 MVP

Files:

  • Create: rider-h5/src/main.ts

  • Create: rider-h5/src/App.vue

  • Create: rider-h5/src/router/index.ts

  • Create: rider-h5/src/api/http.ts

  • Create: rider-h5/src/api/riderTaskApi.ts

  • Create: rider-h5/src/views/LoginView.vue

  • Create: rider-h5/src/views/TaskListView.vue

  • Create: rider-h5/src/views/TaskDetailView.vue

  • Create: rider-h5/src/views/IncomeSummaryView.vue

  • Test: rider-h5/src/api/riderTaskApi.test.ts

  • Step 1: Write task grouping test

import { describe, expect, it } from 'vitest'
import { groupTasksByStatus } from './riderTaskApi'

describe('groupTasksByStatus', () => {
  it('groups assigned delivering and completed tasks', () => {
    const grouped = groupTasksByStatus([
      { id: 1, status: 'ASSIGNED' },
      { id: 2, status: 'DELIVERING' },
      { id: 3, status: 'COMPLETED' }
    ])

    expect(grouped.assigned).toHaveLength(1)
    expect(grouped.delivering).toHaveLength(1)
    expect(grouped.completed).toHaveLength(1)
  })
})
  • Step 2: Implement rider screens

Screens:

  • Login.

  • Today tasks with status tabs.

  • Task detail with user phone, address, order content, remark, and amount.

  • Action buttons: start delivery, delivered, exception, call user.

  • Upload delivered photo through /api/files/upload.

  • Income summary with completed count and estimated service fee.

  • Step 3: Verify

Run:

cd rider-h5
npm install
npm test
npm run build

Expected: tests pass and build succeeds.

  • Step 4: Commit
git add rider-h5
git commit -m "feat: add rider h5 MVP"

Task 12: Integration, Seed Data, And Release Checks

Files:

  • Create: backend/src/main/resources/db/migration/V2__seed_dev_data.sql

  • Create: docs/api/manual-test-checklist.md

  • Create: docs/deploy/local-runbook.md

  • Modify: README.md

  • Step 1: Add development seed data

Create V2__seed_dev_data.sql with:

  • One tenant.

  • One community.

  • Module configs enabled for goods, express, group buy, secondhand, and notice.

  • Admin, merchant, rider, and resident users.

  • One merchant.

  • Six product categories.

  • At least ten products with SKUs.

  • One active group buy.

  • Two notices.

  • One banner.

  • Step 2: Add manual test checklist

Create docs/api/manual-test-checklist.md with these checks:

# Manual Test Checklist

- [ ] Admin can log in.
- [ ] Admin can create and publish a notice.
- [ ] Miniapp can list published notices.
- [ ] Admin can create product and SKU stock.
- [ ] Miniapp can submit goods order.
- [ ] Admin can confirm goods order and mark prepared.
- [ ] Admin can assign delivery order to rider.
- [ ] Rider can start delivery and upload delivered photo.
- [ ] Miniapp can confirm goods order completion.
- [ ] Miniapp can submit express pickup order.
- [ ] Admin can confirm express order and assign rider.
- [ ] Rider can mark express order delivered.
- [ ] Miniapp can submit group buy order before cutoff.
- [ ] Admin can review secondhand listing.
- [ ] Public secondhand list only shows approved listings.
  • Step 3: Add local runbook

Create docs/deploy/local-runbook.md with commands:

docker compose -f deploy/docker-compose.dev.yml up -d
cd backend
mvn spring-boot:run
cd ..\admin-web
npm run dev
cd ..\rider-h5
npm run dev

Expected local URLs:

Backend API: http://localhost:8080
Knife4j docs: http://localhost:8080/doc.html
Admin web: http://localhost:5173
Rider H5: http://localhost:5174
MinIO console: http://localhost:9001
  • Step 4: Run full verification

Run:

docker compose -f deploy/docker-compose.dev.yml up -d
cd backend
mvn test
cd ..\admin-web
npm test
npm run build
cd ..\miniapp
npm test
npm run build
cd ..\rider-h5
npm test
npm run build
git status --short

Expected:

  • Backend tests pass.

  • All frontend tests pass.

  • All builds succeed.

  • git status --short only shows intended changes before final commit.

  • Step 5: Commit

git add backend/src/main/resources/db/migration/V2__seed_dev_data.sql docs README.md
git commit -m "docs: add release runbook and seed data"

Coverage Map

  • PRD roles: covered by Tasks 3, 4, 7, 9, 10, 11.
  • Product reservation: covered by Task 5 and Task 10.
  • Offline payment status: covered by Tasks 5, 6, 9.
  • Admin dispatch: covered by Task 7 and Task 9.
  • Express pickup: covered by Task 6, Task 7, Task 10, Task 11.
  • Group buy: covered by Task 8, Task 9, Task 10.
  • Secondhand review: covered by Task 8, Task 9, Task 10.
  • Notices and banners: covered by Task 4, Task 9, Task 10.
  • SaaS-ready tenant/community fields: covered by Task 2 and enforced by Task 3.
  • Rider H5: covered by Task 7 and Task 11.

Final Acceptance

The MVP is ready for one-community trial operation when:

  • Backend full test suite passes.
  • Admin web, miniapp, and rider H5 builds pass.
  • Manual checklist is completed once against local seed data.
  • Admin can move a goods order from submission to completion through dispatch.
  • Admin can move an express pickup order from submission to completion through dispatch.
  • Miniapp only shows approved secondhand listings and published notices.
  • All committed schema tables include SaaS-ready tenant/community scope where required.