From d3840d621ca8bc6e9c4a7d1d252674da62524a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Tue, 7 Jul 2026 17:08:46 +0800 Subject: [PATCH] chore: add demo profile and local api proxy --- .gitignore | 2 + README.md | 9 + admin-web/.env.example | 2 + admin-web/vite.config.ts | 8 +- .../com/linhelp/dev/DevDataInitializer.java | 243 ++++++++++++++++++ .../src/main/resources/application-demo.yml | 23 ++ .../linhelp/dev/DevDataInitializerTests.java | 43 ++++ docs/deploy/local-runbook.md | 22 ++ miniapp/miniprogram/utils/request.js | 3 +- rider-h5/.env.example | 2 + rider-h5/vite.config.ts | 8 +- 11 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 admin-web/.env.example create mode 100644 backend/src/main/java/com/linhelp/dev/DevDataInitializer.java create mode 100644 backend/src/main/resources/application-demo.yml create mode 100644 backend/src/test/java/com/linhelp/dev/DevDataInitializerTests.java create mode 100644 rider-h5/.env.example diff --git a/.gitignore b/.gitignore index a8f772a..6195f51 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ coverage/ .env .env.* +!.env.example +!**/.env.example backend/logs/ deploy/.data/ diff --git a/README.md b/README.md index 14e8b3f..c228597 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,15 @@ mvn test - Rider H5: `http://localhost:5174` - Resident miniapp: open `miniapp/project.config.json` in WeChat DevTools +No-database demo backend: + +```powershell +cd backend +mvn spring-boot:run "-Dspring-boot.run.profiles=demo" +``` + +Demo profile URL: `http://localhost:18080` + Demo accounts: - Admin: `admin / admin123` diff --git a/admin-web/.env.example b/admin-web/.env.example new file mode 100644 index 0000000..1f07f2b --- /dev/null +++ b/admin-web/.env.example @@ -0,0 +1,2 @@ +VITE_API_BASE_URL= +VITE_API_PROXY_TARGET=http://localhost:8080 diff --git a/admin-web/vite.config.ts b/admin-web/vite.config.ts index 8084277..477d9bf 100644 --- a/admin-web/vite.config.ts +++ b/admin-web/vite.config.ts @@ -4,6 +4,12 @@ import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { - port: 5173 + port: 5173, + proxy: { + '/api': { + target: process.env.VITE_API_PROXY_TARGET || 'http://localhost:8080', + changeOrigin: true + } + } } }) diff --git a/backend/src/main/java/com/linhelp/dev/DevDataInitializer.java b/backend/src/main/java/com/linhelp/dev/DevDataInitializer.java new file mode 100644 index 0000000..cbf27d7 --- /dev/null +++ b/backend/src/main/java/com/linhelp/dev/DevDataInitializer.java @@ -0,0 +1,243 @@ +package com.linhelp.dev; + +import com.linhelp.delivery.DeliveryUserRequest; +import com.linhelp.delivery.DeliveryUserService; +import com.linhelp.groupbuy.GroupBuyRequest; +import com.linhelp.groupbuy.GroupBuyResponse; +import com.linhelp.groupbuy.GroupBuyService; +import com.linhelp.notice.BannerRequest; +import com.linhelp.notice.BannerService; +import com.linhelp.notice.NoticeRequest; +import com.linhelp.notice.NoticeResponse; +import com.linhelp.notice.NoticeService; +import com.linhelp.product.ProductCategoryRequest; +import com.linhelp.product.ProductCategoryResponse; +import com.linhelp.product.ProductRequest; +import com.linhelp.product.ProductResponse; +import com.linhelp.product.ProductService; +import com.linhelp.product.ProductSkuRequest; +import com.linhelp.secondhand.SecondGoodsRequest; +import com.linhelp.secondhand.SecondGoodsResponse; +import com.linhelp.secondhand.SecondGoodsService; +import com.linhelp.user.AddressRequest; +import com.linhelp.user.UserAddressService; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +@Component +@Profile({"dev", "demo"}) +public class DevDataInitializer implements ApplicationRunner { + private static final Long TENANT_ID = 1L; + private static final Long COMMUNITY_ID = 1L; + private static final Long MERCHANT_ID = 1L; + private static final Long RIDER_USER_ID = 3L; + private static final Long RESIDENT_USER_ID = 1000L; + + private final ProductService productService; + private final GroupBuyService groupBuyService; + private final NoticeService noticeService; + private final BannerService bannerService; + private final SecondGoodsService secondGoodsService; + private final DeliveryUserService deliveryUserService; + private final UserAddressService addressService; + + public DevDataInitializer(ProductService productService, + GroupBuyService groupBuyService, + NoticeService noticeService, + BannerService bannerService, + SecondGoodsService secondGoodsService, + DeliveryUserService deliveryUserService, + UserAddressService addressService) { + this.productService = productService; + this.groupBuyService = groupBuyService; + this.noticeService = noticeService; + this.bannerService = bannerService; + this.secondGoodsService = secondGoodsService; + this.deliveryUserService = deliveryUserService; + this.addressService = addressService; + } + + @Override + public void run(ApplicationArguments args) { + if (!productService.listCategories(COMMUNITY_ID, false).isEmpty()) { + return; + } + seedAddress(); + seedDeliveryUser(); + List categories = seedCategories(); + seedProducts(categories); + seedGroupBuy(); + seedNotices(); + seedBanner(); + seedSecondGoods(); + } + + private void seedAddress() { + AddressRequest request = new AddressRequest(); + request.setTenantId(TENANT_ID); + request.setCommunityId(COMMUNITY_ID); + request.setContactName("示范居民"); + request.setPhone("13800001000"); + request.setBuilding("1号楼"); + request.setRoom("1201"); + request.setDetail("东门旁"); + request.setDefault(true); + addressService.create(RESIDENT_USER_ID, request); + } + + private void seedDeliveryUser() { + DeliveryUserRequest request = new DeliveryUserRequest(); + request.setTenantId(TENANT_ID); + request.setCommunityId(COMMUNITY_ID); + request.setUserId(RIDER_USER_ID); + request.setName("配送员小林"); + request.setPhone("13800000003"); + request.setEnabled(true); + deliveryUserService.create(request); + } + + private List seedCategories() { + return Arrays.asList( + createCategory("水果", 10), + createCategory("零食", 20), + createCategory("饮料", 30), + createCategory("早餐", 40), + createCategory("蔬菜", 50), + createCategory("日用品", 60) + ); + } + + private ProductCategoryResponse createCategory(String name, int sortNo) { + ProductCategoryRequest request = new ProductCategoryRequest(); + request.setTenantId(TENANT_ID); + request.setCommunityId(COMMUNITY_ID); + request.setName(name); + request.setSortNo(sortNo); + request.setEnabled(true); + return productService.createCategory(request); + } + + private void seedProducts(List categories) { + createProduct(categories.get(0).getId(), "烟台苹果", "/mock/apple.jpg", "脆甜多汁,适合家庭日常补货", "约1kg", 1290, 1590, 80); + createProduct(categories.get(0).getId(), "海南香蕉", "/mock/banana.jpg", "当日到货,成熟度适中", "约1.5kg", 990, 1290, 70); + createProduct(categories.get(1).getId(), "每日坚果", "/mock/nuts.jpg", "独立小包装,办公室零食", "12包", 2990, 3590, 45); + createProduct(categories.get(1).getId(), "海苔脆片", "/mock/seaweed.jpg", "轻食小零嘴", "原味40g", 690, 890, 90); + createProduct(categories.get(2).getId(), "鲜牛奶", "/mock/milk.jpg", "冷藏配送", "950ml", 1390, 1590, 60); + createProduct(categories.get(2).getId(), "矿泉水", "/mock/water.jpg", "整箱可送上门", "24瓶", 2990, 3590, 30); + createProduct(categories.get(3).getId(), "鲜肉包", "/mock/baozi.jpg", "早餐预订,次日送达", "4个", 800, 1000, 100); + createProduct(categories.get(3).getId(), "豆浆", "/mock/soy-milk.jpg", "现磨豆浆", "300ml", 350, 500, 120); + createProduct(categories.get(4).getId(), "上海青", "/mock/greens.jpg", "新鲜蔬菜,约500g", "约500g", 590, 790, 80); + createProduct(categories.get(5).getId(), "抽纸三包装", "/mock/tissue.jpg", "家庭常备日用品", "3包", 1890, 2290, 55); + } + + private void createProduct(Long categoryId, String name, String coverUrl, String description, + String skuName, int priceCent, int originPriceCent, int stock) { + ProductRequest productRequest = new ProductRequest(); + productRequest.setTenantId(TENANT_ID); + productRequest.setCommunityId(COMMUNITY_ID); + productRequest.setMerchantId(MERCHANT_ID); + productRequest.setCategoryId(categoryId); + productRequest.setName(name); + productRequest.setCoverUrl(coverUrl); + productRequest.setDescription(description); + productRequest.setUnitName("份"); + ProductResponse product = productService.createProduct(productRequest); + + ProductSkuRequest skuRequest = new ProductSkuRequest(); + skuRequest.setTenantId(TENANT_ID); + skuRequest.setCommunityId(COMMUNITY_ID); + skuRequest.setSkuName(skuName); + skuRequest.setPriceCent(priceCent); + skuRequest.setOriginPriceCent(originPriceCent); + skuRequest.setStock(stock); + skuRequest.setEnabled(true); + productService.addSku(product.getId(), skuRequest); + productService.onShelf(product.getId()); + } + + private void seedGroupBuy() { + GroupBuyRequest request = new GroupBuyRequest(); + request.setTenantId(TENANT_ID); + request.setCommunityId(COMMUNITY_ID); + request.setTitle("今日团购:麒麟西瓜"); + request.setCoverUrl("/mock/watermelon.jpg"); + request.setDescription("满20份成团,晚饭前送到小区门口"); + request.setPriceCent(1990); + request.setOriginPriceCent(2590); + request.setStock(120); + request.setStartTime(LocalDateTime.now().minusHours(1)); + request.setEndTime(LocalDateTime.now().plusDays(1)); + request.setPickupAddress("小区东门团购点"); + GroupBuyResponse groupBuy = groupBuyService.create(request); + groupBuyService.start(groupBuy.getId()); + } + + private void seedNotices() { + NoticeResponse first = noticeService.create(notice("本周六小区便民服务日", "物业联合邻小帮提供快递代取、生活用品预订和家电检测服务。", "ACTIVITY", true)); + NoticeResponse second = noticeService.create(notice("地下车库临时清洗通知", "本周三9:00-12:00清洗地下车库,请车主提前挪车。", "PROPERTY", false)); + noticeService.publish(first.getId()); + noticeService.publish(second.getId()); + } + + private NoticeRequest notice(String title, String content, String category, boolean pinned) { + NoticeRequest request = new NoticeRequest(); + request.setTenantId(TENANT_ID); + request.setCommunityId(COMMUNITY_ID); + request.setTitle(title); + request.setContent(content); + request.setCategory(category); + request.setPinned(pinned); + return request; + } + + private void seedBanner() { + BannerRequest request = new BannerRequest(); + request.setTenantId(TENANT_ID); + request.setCommunityId(COMMUNITY_ID); + request.setTitle("邻小帮今日到家"); + request.setImageUrl("/mock/banner-home.jpg"); + request.setLinkType("PAGE"); + request.setLinkValue("/pages/products/index"); + request.setSortNo(10); + request.setEnabled(true); + bannerService.create(request); + } + + private void seedSecondGoods() { + SecondGoodsResponse published = secondGoodsService.create(RESIDENT_USER_ID, secondGoods( + "九成新儿童自行车", + 12000, + "适合5-8岁儿童,小区内可面交。", + "母婴", + Arrays.asList("/mock/bike.jpg") + )); + secondGoodsService.approve(published.getId()); + secondGoodsService.create(RESIDENT_USER_ID, secondGoods( + "闲置置物架", + 3000, + "待后台审核的示例闲置。", + "家居", + Arrays.asList("/mock/shelf.jpg") + )); + } + + private SecondGoodsRequest secondGoods(String title, int priceCent, String description, String category, List images) { + SecondGoodsRequest request = new SecondGoodsRequest(); + request.setTenantId(TENANT_ID); + request.setCommunityId(COMMUNITY_ID); + request.setTitle(title); + request.setImageUrls(images); + request.setPriceCent(priceCent); + request.setDescription(description); + request.setCategory(category); + request.setContactPhone("13800001000"); + request.setTradeMethod("FACE_TO_FACE"); + return request; + } +} diff --git a/backend/src/main/resources/application-demo.yml b/backend/src/main/resources/application-demo.yml new file mode 100644 index 0000000..82106ab --- /dev/null +++ b/backend/src/main/resources/application-demo.yml @@ -0,0 +1,23 @@ +spring: + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration + flyway: + enabled: false + +server: + port: 18080 + +knife4j: + enable: true + +sa-token: + token-name: Authorization + timeout: 2592000 + +linhelp: + storage: + endpoint: http://localhost:9000 + access-key: linhelp + secret-key: linhelp123 + bucket: linhelp diff --git a/backend/src/test/java/com/linhelp/dev/DevDataInitializerTests.java b/backend/src/test/java/com/linhelp/dev/DevDataInitializerTests.java new file mode 100644 index 0000000..3950ffb --- /dev/null +++ b/backend/src/test/java/com/linhelp/dev/DevDataInitializerTests.java @@ -0,0 +1,43 @@ +package com.linhelp.dev; + +import com.linhelp.delivery.DeliveryUserService; +import com.linhelp.groupbuy.GroupBuyService; +import com.linhelp.notice.BannerService; +import com.linhelp.notice.NoticeService; +import com.linhelp.product.ProductService; +import com.linhelp.secondhand.SecondGoodsService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = { + "spring.flyway.enabled=false", + "spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration" +}) +class DevDataInitializerTests { + @Autowired + private ProductService productService; + @Autowired + private GroupBuyService groupBuyService; + @Autowired + private NoticeService noticeService; + @Autowired + private BannerService bannerService; + @Autowired + private SecondGoodsService secondGoodsService; + @Autowired + private DeliveryUserService deliveryUserService; + + @Test + void seedsOneCommunityDemoDataForLocalTrial() { + assertThat(productService.listCategories(1L, true)).hasSize(6); + assertThat(productService.listMini(1L, null)).hasSizeGreaterThanOrEqualTo(10); + assertThat(groupBuyService.listMini(1L)).hasSize(1); + assertThat(noticeService.listPublished(1L)).hasSize(2); + assertThat(bannerService.listEnabled(1L)).hasSize(1); + assertThat(secondGoodsService.listPublic(1L, null)).hasSize(1); + assertThat(deliveryUserService.requireByUserId(3L).getName()).isEqualTo("配送员小林"); + } +} diff --git a/docs/deploy/local-runbook.md b/docs/deploy/local-runbook.md index f0ff16a..ed71ff6 100644 --- a/docs/deploy/local-runbook.md +++ b/docs/deploy/local-runbook.md @@ -37,6 +37,19 @@ Backend API: http://localhost:8080 Knife4j docs: http://localhost:8080/doc.html ``` +If Docker/MySQL are not available or port `8080` is already occupied, use the no-database demo profile: + +```powershell +cd backend +mvn spring-boot:run "-Dspring-boot.run.profiles=demo" +``` + +Demo profile URL: + +```text +Backend API: http://localhost:18080 +``` + Demo accounts: ```text @@ -60,6 +73,13 @@ URL: http://localhost:5173 ``` +During local development, `/api` is proxied to `http://localhost:8080` by default. If the backend uses another port: + +```powershell +$env:VITE_API_PROXY_TARGET="http://localhost:18080" +npm run dev +``` + ## Start Rider H5 ```powershell @@ -74,6 +94,8 @@ URL: http://localhost:5174 ``` +Rider H5 uses the same `/api` dev proxy behavior as the admin web. + ## Open Miniapp Use WeChat DevTools to open: diff --git a/miniapp/miniprogram/utils/request.js b/miniapp/miniprogram/utils/request.js index 15584b8..7c6f8fa 100644 --- a/miniapp/miniprogram/utils/request.js +++ b/miniapp/miniprogram/utils/request.js @@ -8,7 +8,8 @@ function ensureToken() { url: app.globalData.apiBaseUrl + '/api/auth/dev-miniapp-login', method: 'POST', data: { - username: 'resident' + username: 'resident', + password: 'dev' }, success(res) { if (res.statusCode >= 200 && res.statusCode < 300 && res.data && res.data.code === 0) { diff --git a/rider-h5/.env.example b/rider-h5/.env.example new file mode 100644 index 0000000..1f07f2b --- /dev/null +++ b/rider-h5/.env.example @@ -0,0 +1,2 @@ +VITE_API_BASE_URL= +VITE_API_PROXY_TARGET=http://localhost:8080 diff --git a/rider-h5/vite.config.ts b/rider-h5/vite.config.ts index d282194..7cf717c 100644 --- a/rider-h5/vite.config.ts +++ b/rider-h5/vite.config.ts @@ -4,6 +4,12 @@ import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { - port: 5174 + port: 5174, + proxy: { + '/api': { + target: process.env.VITE_API_PROXY_TARGET || 'http://localhost:8080', + changeOrigin: true + } + } } })