2082 lines
59 KiB
Markdown
2082 lines
59 KiB
Markdown
# 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
|
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
<modelVersion>4.0.0</modelVersion>
|
|
<groupId>com.linhelp</groupId>
|
|
<artifactId>linhelp-backend</artifactId>
|
|
<version>0.1.0-SNAPSHOT</version>
|
|
<name>linhelp-backend</name>
|
|
<properties>
|
|
<java.version>17</java.version>
|
|
<spring-boot.version>3.3.2</spring-boot.version>
|
|
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
|
<sa-token.version>1.38.0</sa-token.version>
|
|
<knife4j.version>4.5.0</knife4j.version>
|
|
<minio.version>8.5.11</minio.version>
|
|
</properties>
|
|
<dependencyManagement>
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-dependencies</artifactId>
|
|
<version>${spring-boot.version}</version>
|
|
<type>pom</type>
|
|
<scope>import</scope>
|
|
</dependency>
|
|
</dependencies>
|
|
</dependencyManagement>
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-web</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-validation</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>com.baomidou</groupId>
|
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
|
<version>${mybatis-plus.version}</version>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>cn.dev33</groupId>
|
|
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
|
<version>${sa-token.version}</version>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>com.mysql</groupId>
|
|
<artifactId>mysql-connector-j</artifactId>
|
|
<scope>runtime</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.flywaydb</groupId>
|
|
<artifactId>flyway-core</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.flywaydb</groupId>
|
|
<artifactId>flyway-mysql</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>com.github.xiaoymin</groupId>
|
|
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
|
<version>${knife4j.version}</version>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>io.minio</groupId>
|
|
<artifactId>minio</artifactId>
|
|
<version>${minio.version}</version>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-test</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
</dependencies>
|
|
<build>
|
|
<plugins>
|
|
<plugin>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
</plugin>
|
|
</plugins>
|
|
</build>
|
|
</project>
|
|
```
|
|
|
|
- [ ] **Step 2: Add Spring Boot entrypoint**
|
|
|
|
Create `backend/src/main/java/com/linhelp/LinHelpApplication.java`.
|
|
|
|
```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<T>(int code, String message, T data) {
|
|
public static <T> ApiResponse<T> ok(T data) {
|
|
return new ApiResponse<>(0, "ok", data);
|
|
}
|
|
|
|
public static ApiResponse<Void> ok() {
|
|
return new ApiResponse<>(0, "ok", null);
|
|
}
|
|
|
|
public static ApiResponse<Void> fail(int code, String message) {
|
|
return new ApiResponse<>(code, message, null);
|
|
}
|
|
}
|
|
```
|
|
|
|
Create `PageResponse.java`.
|
|
|
|
```java
|
|
package com.linhelp.common.api;
|
|
|
|
import java.util.List;
|
|
|
|
public record PageResponse<T>(long total, long pageNo, long pageSize, List<T> records) {
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Create status enums with transition rules**
|
|
|
|
Create `GoodsOrderStatus.java`.
|
|
|
|
```java
|
|
package com.linhelp.common.enums;
|
|
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
public enum GoodsOrderStatus {
|
|
PENDING_CONFIRM,
|
|
PREPARING,
|
|
PENDING_DELIVERY,
|
|
PENDING_PICKUP,
|
|
DELIVERING,
|
|
COMPLETED,
|
|
CANCELED;
|
|
|
|
private static final Map<GoodsOrderStatus, Set<GoodsOrderStatus>> TRANSITIONS = Map.of(
|
|
PENDING_CONFIRM, Set.of(PREPARING, CANCELED),
|
|
PREPARING, Set.of(PENDING_DELIVERY, PENDING_PICKUP, CANCELED),
|
|
PENDING_DELIVERY, Set.of(DELIVERING, CANCELED),
|
|
PENDING_PICKUP, Set.of(COMPLETED, CANCELED),
|
|
DELIVERING, Set.of(COMPLETED, CANCELED)
|
|
);
|
|
|
|
public boolean canMoveTo(GoodsOrderStatus target) {
|
|
return TRANSITIONS.getOrDefault(this, Set.of()).contains(target);
|
|
}
|
|
}
|
|
```
|
|
|
|
Create the remaining enum files with matching names from "Shared Domain Vocabulary". Give `ExpressOrderStatus` the same `canMoveTo` method and a transition map for `PENDING_CONFIRM -> PENDING_PICKUP -> PICKING_UP -> DELIVERING -> DELIVERED -> COMPLETED`, with cancellation allowed before completion.
|
|
|
|
- [ ] **Step 5: Create initial schema**
|
|
|
|
Create `backend/src/main/resources/db/migration/V1__init_schema.sql`.
|
|
|
|
```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> 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<AddressResponse> addresses = addressService.listMine(userId);
|
|
|
|
assertThat(addresses).filteredOn(AddressResponse::isDefault).singleElement()
|
|
.extracting(AddressResponse::id).isEqualTo(second.id());
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Implement user address APIs**
|
|
|
|
Create endpoints:
|
|
|
|
```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.
|