# 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 2.7.18, Java 8, MyBatis Plus, Sa-Token, MySQL 8, Redis, Flyway, Knife4j/OpenAPI, MinIO-compatible object storage, Vue 3, TypeScript, Vite, Element Plus for admin web, WeChat native mini program with WXML/WXSS/JavaScript, 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 │ ├── project.config.json │ └── miniprogram │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── api │ ├── components │ ├── pages │ └── utils ├── 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. - Admin web and rider H5 use API modules under `src/api`; the native mini program uses request modules under `miniprogram/api` and a shared `miniprogram/utils/request.js` wrapper instead of inline `wx.request` 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, admin web, rider H5, and miniapp constants: ```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: `miniapp/project.config.json` - Create: `miniapp/miniprogram/app.js` - Create: `miniapp/miniprogram/app.json` - Create: `miniapp/miniprogram/app.wxss` - Create: `miniapp/miniprogram/utils/request.js` - 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 8, Spring Boot 2.7.18, MyBatis Plus, Sa-Token, MySQL, Redis, Flyway, Knife4j, MinIO, validation, and test dependencies. Spring Boot 3 is not used because it requires Java 17+. ```xml 4.0.0 com.linhelp linhelp-backend 0.1.0-SNAPSHOT linhelp-backend 1.8 1.8 1.8 UTF-8 2.7.18 3.5.5 1.38.0 4.4.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-boot-starter ${mybatis-plus.version} cn.dev33 sa-token-spring-boot-starter ${sa-token.version} mysql mysql-connector-java runtime org.flywaydb flyway-core com.github.xiaoymin knife4j-openapi3-spring-boot-starter ${knife4j.version} io.minio minio ${minio.version} org.springframework.boot spring-boot-starter-test test org.apache.maven.plugins maven-compiler-plugin 1.8 1.8 UTF-8 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 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` for native mini program helper tests and optional WeChat CI commands. Do not add uni-app, Vue, or Vite dependencies to the miniapp. ```json { "name": "linhelp-miniapp", "version": "0.1.0", "private": true, "scripts": { "test": "jest --runInBand", "ci:preview": "miniprogram-ci preview --pp ./project.config.json --robot 1" }, "devDependencies": { "jest": "^29.7.0", "miniprogram-ci": "^2.0.0" } } ``` Create `miniapp/project.config.json`. ```json { "appid": "touristappid", "projectname": "linhelp-miniapp", "miniprogramRoot": "miniprogram/", "setting": { "urlCheck": false, "es6": true, "enhance": true, "postcss": true, "minified": true }, "compileType": "miniprogram" } ``` Create `miniapp/miniprogram/app.js`. ```js App({ globalData: { apiBaseUrl: 'http://localhost:8080', token: '' } }) ``` Create `miniapp/miniprogram/app.json`. ```json { "pages": [ "pages/home/index", "pages/products/index", "pages/products/detail", "pages/orders/index", "pages/orders/detail", "pages/express/create", "pages/group-buy/index", "pages/group-buy/detail", "pages/second-hand/index", "pages/second-hand/create", "pages/notices/index", "pages/profile/index", "pages/address/index" ], "window": { "navigationBarTitleText": "邻小帮", "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black", "backgroundColor": "#f6f7f9" }, "tabBar": { "color": "#6b7280", "selectedColor": "#0f766e", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/home/index", "text": "首页" }, { "pagePath": "pages/group-buy/index", "text": "团购" }, { "pagePath": "pages/second-hand/index", "text": "闲置" }, { "pagePath": "pages/profile/index", "text": "我的" } ] } } ``` Create `miniapp/miniprogram/app.wxss`. ```css page { background: #f6f7f9; color: #111827; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; } .page { padding: 24rpx; } ``` Create `miniapp/miniprogram/utils/request.js`. ```js function request(options) { const app = getApp() const token = app.globalData.token return new Promise((resolve, reject) => { wx.request({ url: app.globalData.apiBaseUrl + options.url, method: options.method || 'GET', data: options.data || {}, header: Object.assign({ Authorization: token }, options.header || {}), success(res) { if (res.statusCode >= 200 && res.statusCode < 300 && res.data && res.data.code === 0) { resolve(res.data.data) return } reject(new Error((res.data && res.data.message) || '请求失败')) }, fail: reject }) }) } module.exports = { request } ``` 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`. Use a normal Java class because the backend targets Java 8. ```java package com.linhelp.common.api; public class ApiResponse { private int code; private String message; private T data; public ApiResponse() { } public ApiResponse(int code, String message, T data) { this.code = code; this.message = message; this.data = 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); } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } } ``` Create `PageResponse.java`. ```java package com.linhelp.common.api; import java.util.List; public class PageResponse { private long total; private long pageNo; private long pageSize; private List records; public PageResponse() { } public PageResponse(long total, long pageNo, long pageSize, List records) { this.total = total; this.pageNo = pageNo; this.pageSize = pageSize; this.records = records; } public long getTotal() { return total; } public void setTotal(long total) { this.total = total; } public long getPageNo() { return pageNo; } public void setPageNo(long pageNo) { this.pageNo = pageNo; } public long getPageSize() { return pageSize; } public void setPageSize(long pageSize) { this.pageSize = pageSize; } public List getRecords() { return records; } public void setRecords(List records) { this.records = records; } } ``` - [ ] **Step 4: Create status enums with transition rules** Create `GoodsOrderStatus.java`. ```java package com.linhelp.common.enums; import java.util.Collections; import java.util.EnumMap; import java.util.EnumSet; 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; static { Map> transitions = new EnumMap>(GoodsOrderStatus.class); transitions.put(PENDING_CONFIRM, EnumSet.of(PREPARING, CANCELED)); transitions.put(PREPARING, EnumSet.of(PENDING_DELIVERY, PENDING_PICKUP, CANCELED)); transitions.put(PENDING_DELIVERY, EnumSet.of(DELIVERING, CANCELED)); transitions.put(PENDING_PICKUP, EnumSet.of(COMPLETED, CANCELED)); transitions.put(DELIVERING, EnumSet.of(COMPLETED, CANCELED)); TRANSITIONS = Collections.unmodifiableMap(transitions); } public boolean canMoveTo(GoodsOrderStatus target) { Set targets = TRANSITIONS.get(this); return targets != null && targets.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 static class Scope { private final Long tenantId; private final Long communityId; private Scope(Long tenantId, Long communityId) { this.tenantId = tenantId; this.communityId = communityId; } private Long tenantId() { return tenantId; } private Long communityId() { return 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 class LoginRequest { private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } public class LoginResponse { private String token; private Long userId; private Long tenantId; private Long communityId; private String roleCode; public LoginResponse(String token, Long userId, Long tenantId, Long communityId, String roleCode) { this.token = token; this.userId = userId; this.tenantId = tenantId; this.communityId = communityId; this.roleCode = roleCode; } public String getToken() { return token; } public Long getUserId() { return userId; } public Long getTenantId() { return tenantId; } public Long getCommunityId() { return communityId; } public String getRoleCode() { return 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, Collections.singletonList(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/miniprogram/api/http.js` - Create: `miniapp/miniprogram/api/homeApi.js` - Create: `miniapp/miniprogram/api/productApi.js` - Create: `miniapp/miniprogram/api/orderApi.js` - Create: `miniapp/miniprogram/api/expressApi.js` - Create: `miniapp/miniprogram/api/groupBuyApi.js` - Create: `miniapp/miniprogram/api/secondGoodsApi.js` - Create: `miniapp/miniprogram/api/noticeApi.js` - Create: `miniapp/miniprogram/api/addressApi.js` - Create: `miniapp/miniprogram/pages/home/index.js` - Create: `miniapp/miniprogram/pages/home/index.json` - Create: `miniapp/miniprogram/pages/home/index.wxml` - Create: `miniapp/miniprogram/pages/home/index.wxss` - Create: native page file sets under `miniapp/miniprogram/pages/products`, `orders`, `express`, `group-buy`, `second-hand`, `notices`, `profile`, and `address` - Test: `miniapp/miniprogram/api/expressApi.test.js` - [ ] **Step 1: Create express fee UI helper test** ```js const { calculateExpressFeeCent } = require('./expressApi') describe('calculateExpressFeeCent', () => { test('matches backend express fee rule', () => { expect(calculateExpressFeeCent(1)).toBe(300) expect(calculateExpressFeeCent(2)).toBe(400) expect(calculateExpressFeeCent(4)).toBe(600) }) }) ``` - [ ] **Step 2: Implement native miniapp API modules** Create `miniapp/miniprogram/api/expressApi.js`. ```js const { request } = require('../utils/request') function calculateExpressFeeCent(packageCount) { if (packageCount <= 0) { throw new Error('件数必须大于0') } return 300 + Math.max(0, packageCount - 1) * 100 } function createExpressOrder(data) { return request({ url: '/api/mini/express-orders', method: 'POST', data: data }) } function listExpressOrders() { return request({ url: '/api/mini/express-orders' }) } module.exports = { calculateExpressFeeCent, createExpressOrder, listExpressOrders } ``` Create matching CommonJS modules for products, orders, group buy, secondhand, notices, and addresses. Each module exports named functions and calls `request()` from `miniprogram/utils/request.js`. - [ ] **Step 3: Implement miniapp navigation** Tabs: ```text 首页 团购 闲置 我的 ``` Home shortcuts: ```text 商品预定 代取快递 今日团购 二手闲置 社区公告 我的订单 ``` - [ ] **Step 4: Implement native miniapp page pattern** Use this structure for every page: ```text index.js page state, lifecycle methods, event handlers index.json navigation title and page-level component config index.wxml native miniapp markup index.wxss page styles ``` Example `miniapp/miniprogram/pages/express/create.js`: ```js const { calculateExpressFeeCent, createExpressOrder } = require('../../api/expressApi') Page({ data: { form: { expressCompany: '', pickupCode: '', pickupAddress: '', addressId: null, phone: '', packageCount: 1, remark: '' }, feeCent: 300, submitting: false }, onPackageCountChange(event) { const packageCount = Number(event.detail.value || 1) this.setData({ 'form.packageCount': packageCount, feeCent: calculateExpressFeeCent(packageCount) }) }, submit() { if (this.data.submitting) { return } this.setData({ submitting: true }) createExpressOrder(this.data.form) .then(() => { wx.showToast({ title: '已提交' }) wx.navigateBack() }) .catch((error) => { wx.showToast({ title: error.message, icon: 'none' }) }) .finally(() => { this.setData({ submitting: false }) }) } }) ``` Example `miniapp/miniprogram/pages/express/create.wxml`: ```xml 预估费用:{{feeCent / 100}}元 ``` - [ ] **Step 5: 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 6: Verify** Run: ```powershell cd miniapp npm install npm test ``` Expected: Jest helper tests pass. Open `miniapp/project.config.json` in WeChat DevTools and click Compile; expected result is that the native mini program compiles with no missing page, JSON, WXML, WXSS, or API module errors. - [ ] **Step 7: 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 cd ..\rider-h5 npm test npm run build git status --short ``` Expected: - Backend tests pass. - All frontend tests pass. - Admin web and rider H5 builds succeed. - Native miniapp helper tests pass, and `miniapp/project.config.json` compiles successfully in WeChat DevTools. - `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 and rider H5 builds pass. - Native miniapp compiles successfully in WeChat DevTools. - 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.