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.