Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ MYSQL_ROOT_PASSWORD=replace-with-generated-root-password
APP_JWT_SECRET=replace-with-generated-jwt-secret-at-least-32-characters
HTTP_PORT=18080
BACKEND_PORT=18081
YOUYU_CORS_ALLOWED_ORIGINS=http://localhost:18080,http://127.0.0.1:18080
YOUYU_CORS_ALLOWED_ORIGIN_PATTERNS=

# Optional Alipay sandbox verification. Leave disabled for the default mock flow.
# When enabled, ALIPAY_SANDBOX_NOTIFY_URL must be a stable public HTTPS URL
# that routes to this backend, for example:
# https://demo.example.com/api/payments/callbacks/alipay-sandbox
# A localhost, container hostname, or expired tunnel URL cannot receive Alipay callbacks.
ALIPAY_SANDBOX_ENABLED=false
ALIPAY_SANDBOX_APP_ID=
ALIPAY_SANDBOX_PRIVATE_KEY=
Expand All @@ -26,8 +32,27 @@ MEILISEARCH_API_KEY=
MEILISEARCH_PRODUCT_INDEX=youyu_products
MEILISEARCH_TIMEOUT=2s

# Optional Redis cache candidate for launch-performance rehearsal.
# Compose starts Redis, but backend cache usage and Redis health are disabled
# until these switches are explicitly enabled.
REDIS_PORT=16379
REDIS_PASSWORD=
REDIS_MAXMEMORY=256mb
REDIS_MAXMEMORY_POLICY=allkeys-lru
REDIS_TIMEOUT=2s
REDIS_POOL_MAX_ACTIVE=20
REDIS_POOL_MAX_IDLE=10
REDIS_POOL_MIN_IDLE=0
YOUYU_REDIS_CACHE_ENABLED=false
YOUYU_REDIS_HEALTH_ENABLED=false
YOUYU_REDIS_CACHE_KEY_PREFIX=youyu:cache:
YOUYU_REDIS_HOT_SEARCH_TTL=1h
YOUYU_REDIS_HOME_RECOMMEND_TTL=15m
YOUYU_REDIS_ALSO_BOUGHT_TTL=15m

# Optional logistics tracking and map-provider configuration.
# Amap WebService key is backend-only. Frontend Amap JS keys live in frontend/.env.*.
# Amap WebService key is backend-only. Frontend Amap JS keys are public
# browser build-time values; restrict them by domain before any real deployment.
YOUYU_AMAP_ENABLED=false
YOUYU_AMAP_WEB_SERVICE_KEY=
YOUYU_LOGISTICS_TRACKING_ENABLED=false
Expand All @@ -36,3 +61,16 @@ KDNIAO_BUSINESS_ID=
KDNIAO_APP_KEY=
KDNIAO_ENDPOINT=https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx
YOUYU_LOGISTICS_TRACKING_TIMEOUT_SECONDS=5

# Optional frontend map rendering build args forwarded by Compose.
VITE_AMAP_JS_KEY=
VITE_AMAP_SECURITY_CODE=

# Optional SMTP delivery. Required for public registration and password reset
# email delivery in a non-test public environment.
APP_MAIL_HOST=
APP_MAIL_PORT=465
APP_MAIL_USERNAME=
APP_MAIL_PASSWORD=
APP_MAIL_FROM=
APP_MAIL_SSL_ENABLED=true
98 changes: 98 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,101 @@
## [2026-06-04] - Docker Demo Upload And Shop Detail Stabilization

### changed
- Raised the frontend Nginx upload limit and proxied `/uploads/` to the backend so avatar uploads and uploaded media work through Docker.
- Persisted backend uploaded avatars in a Docker volume and created writable upload directories in the backend image.
- Enabled the Docker demo seed profile by default, fixed the MySQL `DROP INDEX` syntax, and removed conflicting demo shop seed rows that left products pointing at missing shops.
- Added frontend image fallbacks and restored payment-page product snapshots so product media degrades cleanly when remote images fail.
- Documented the Alipay sandbox public HTTPS callback requirement.

### verify
- `docker compose build backend`
- `docker compose up -d --force-recreate backend frontend`
- Avatar upload through `http://localhost:18080/api/users/me/avatar` returned success and the uploaded `/uploads/avatars/...png` URL returned HTTP 200.
- `http://localhost:18080/api/shops/4203` returned success with 300 products.
- Demo seed verification reported 2400 expansion products and no orphan product shop references.

---

## [2026-06-04] - Profile Spend Chart Category Consolidation

### changed
- Limited the profile spend-category donut chart to the top 5 spending categories and merged the remaining long-tail categories into a single `其他` slice so the legend no longer overruns the card.
- Normalized category chart input inside `SpendChart.vue` before rendering, keeping monthly and yearly spend views unchanged.

### verify
- Frontend `npm run build`

---

## [2026-06-04] - Demo Dataset Expansion And Refresh

### changed
- Replaced the old optional realistic-product load step with a generated `seed/demo-expansion.sql` dataset that adds 2400 more presentation-ready products, richer buyer/seller lifecycles, denser admin/support/message data, and dashboard-friendly order coverage.
- Cleaned visible demo-facing names in the base seed assets so storefronts, users, product cards, support tickets, spend history, and chat threads no longer expose raw `seed` or stress-test wording.
- Added a demo-only MySQL volume override in `compose.demo.yml` and a `scripts/run-demo-fresh.ps1` helper so the Docker demo stack can be reset and reseeded from scratch in one step.
- Documented the new demo-seed workflow in backend and scripts docs and recorded the cross-cutting execution task.

### verify
- `node scripts/generate-demo-expansion-sql.mjs` passed and regenerated `backend/src/main/resources/seed/demo-expansion.sql`.
- `git diff --check` passed.
- Seed-asset search confirmed no remaining visible `Seed Buyer`, `Seed Secondhand`, `SEED-SUP`, `Reviewed Product`, or `Scenario Product` strings across the active demo seed files.

---

## [2026-06-04] - Launch Preparation Redis Cache Candidate

### changed
- Added optional Redis-backed cache support for hot search, home recommendations, and also-bought recommendations with configurable TTLs and fallback to existing JDBC paths.
- Added cache invalidation for search-governance mutations, product publish/update/status/delete mutations, order completion, and product-review submission.
- Added Redis service and cache environment switches to Compose and `.env.example`, including optional Redis password, memory ceiling, and `allkeys-lru` eviction policy.
- Added Compose CORS example forwarding and fixed the frontend Docker build for Alpine/Rollup optional native dependencies so the demo stack can start from the checked-in Compose files.
- Added frontend product-list pagination protection so a stale external search index cannot trigger unbounded product-page loading during local rehearsals.
- Fixed logistics map render timing so the Amap panel initializes after the Vue container is mounted in the local Compose frontend, and added a Northeast University Hunnan campus address marker fallback for local orders without courier coordinates.
- Documented Redis rehearsal behavior in backend docs, launch runbook, and search/recommend API specs.

### verify
- Backend `.\mvnw.cmd test -Dtest=RecommendServiceCacheTest,SearchServiceCacheTest` passed, 6 tests.
- Backend `.\mvnw.cmd test` passed, 248 tests.
- Frontend Docker image build passed after the Rollup native dependency fix, with existing chunk-size warnings.
- Compose demo stack with Redis password, cache enabled, and Redis health enabled started successfully; `/actuator/health` reported Redis `UP`, cache candidate endpoints returned success, and Redis contained `youyu:cache:search:hot`, `youyu:cache:recommend:home:anonymous:8`, and `youyu:cache:recommend:also-bought:3001:6`.
- Redis container configuration confirmed `appendonly yes`, `maxmemory 256mb`, and `maxmemory-policy allkeys-lru`.
- Browser smoke for `/app/products` passed after disabling external Meilisearch in local `.env`; the page loaded one product-list request and displayed 11 local seed products.
- `docker compose --env-file .env.example config` passed.
- `git diff --check` passed.

---

## [2026-06-04] - Launch Preparation Wave 1

### changed
- Added production-like CORS validation, mock-auth profile gating, and mock-payment fail-fast protection while preserving local dev/seed/test behavior.
- Added legal document pages, cookie consent, registration agreement enforcement, consent logs, personal data export, and soft account closure.
- Added staging Hikari configuration, database-aware `/api/health`, restricted Actuator health exposure, and non-destructive backup helper defaults.
- Forwarded documented optional runtime variables through Compose and Docker build args while keeping default staging startup separate from the demo seed overlay.
- Documented launch security scan commands, SQL/sort review evidence, mock JWT/payment boundaries, runtime validation, rollback steps, and external HTTPS/payment-production blockers.
- Added security hardening HTTP smoke requests for CORS, protected endpoints, admin permission checks, and payment gateway exposure.

### verify
- Backend `.\mvnw.cmd test` passed, 242 tests.
- Frontend `npm test` passed, 63 tests.
- Frontend `npm run build` passed, with existing Rollup/chunk-size warnings.
- `docker compose config` passed with temporary example `DB_PASSWORD`, `MYSQL_ROOT_PASSWORD`, and `APP_JWT_SECRET` values.
- `git diff --check` passed.

---

## [2026-06-04] - Launch Preparation Task Activation

### changed
- Activated the launch-preparation task program for the L0-L7 roadmap, with separate active tasks for security, privacy compliance, runtime infrastructure, container deployment, and integration verification.
- Reconciled stale active feature-polish task copies by moving them into archived `*-stale-active-copy.md` records while preserving completed task history.
- Recorded that the repository is ready for launch-preparation execution, but public production launch remains blocked by the roadmap P0 items and external operations.

### verify
- Documentation-only L0 transition; active task directory now contains the launch-preparation task set.

---

## [2026-06-04] - Search And Logistics Seed Samples

### changed
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ npm run build
mvnw.cmd test
```

## Docker Demo Notes

The Compose stack exposes the frontend through Nginx on `HTTP_PORT` and proxies
`/api/` plus uploaded media under `/uploads/` to the backend container.

For a fresh seeded demo environment:

```bash
.\scripts\run-demo-fresh.ps1 -Detach
```

Runtime uploads are stored in the `backend-uploads` Docker volume. If Alipay
sandbox is enabled, `ALIPAY_SANDBOX_NOTIFY_URL` must be a stable public HTTPS
URL that reaches `/api/payments/callbacks/alipay-sandbox`; localhost, container
hostnames, and expired tunnel URLs cannot receive Alipay callbacks.

### End-to-end

From `frontend/`:
Expand Down
4 changes: 3 additions & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

RUN addgroup -S spring && adduser -S spring -G spring
RUN addgroup -S spring && adduser -S spring -G spring \
&& mkdir -p /app/data/uploads/avatars \
&& chown -R spring:spring /app/data

COPY --from=builder /workspace/target/youyu-backend-*.jar /app/app.jar

Expand Down
36 changes: 36 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ Spring Boot 3.3 backend for Youyu. Layered by domain: `controller / service / ma
|------|----------|---------|-------------|
| `MYSQL_PASSWORD` | local: optional, deploy: yes | `yinkaixin123` | MySQL password for the `youyu` schema |
| `APP_JWT_SECRET` | local: optional, deploy: **yes** | committed dev secret | JWT signing secret (≥32 chars); falls back to a dev default when running with profile `dev` / `seed` / `test` |
| `YOUYU_CORS_ALLOWED_ORIGINS` | staging/prod: **yes** | empty | Comma-separated exact frontend origins allowed by CORS, for example `https://www.example.com` |
| `YOUYU_CORS_ALLOWED_ORIGIN_PATTERNS` | staging/prod: optional | empty | Comma-separated CORS origin patterns; avoid this unless exact origins are not sufficient |
| `ALIPAY_SANDBOX_ENABLED` | payment rehearsal: optional | `false` | Enables Alipay sandbox gateway when fully configured |
| `ALIPAY_SANDBOX_APP_ID` | Alipay sandbox: yes when enabled | none | Sandbox application ID |
| `ALIPAY_SANDBOX_PRIVATE_KEY` | Alipay sandbox: yes when enabled | none | Application private key; keep private |
| `ALIPAY_SANDBOX_PUBLIC_KEY` | Alipay sandbox: yes when enabled | none | Alipay public key used to verify responses and callbacks |
| `ALIPAY_SANDBOX_NOTIFY_URL` | Alipay sandbox: yes when enabled | none | Stable public HTTPS callback URL ending in `/api/payments/callbacks/alipay-sandbox` |
| `ALIPAY_SANDBOX_GATEWAY_URL` | Alipay sandbox: optional | Alipay sandbox URL | Override only for approved sandbox endpoints |
| `APP_MAIL_HOST` | runtime email delivery: **yes** | none | SMTP server host |
| `APP_MAIL_PORT` | runtime email delivery: **yes** | none | SMTP server port |
| `APP_MAIL_USERNAME` | runtime email delivery: **yes** | none | SMTP login username |
Expand All @@ -26,6 +34,16 @@ Spring Boot 3.3 backend for Youyu. Layered by domain: `controller / service / ma
| `KDNIAO_APP_KEY` | Kdniao tracking: yes when enabled | none | Kdniao app key; keep private |
| `KDNIAO_ENDPOINT` | Kdniao tracking: optional | Kdniao API URL | Override only for approved provider endpoints |
| `YOUYU_LOGISTICS_TRACKING_TIMEOUT_SECONDS` | real logistics tracking: optional | `5` | Provider request timeout |
| `YOUYU_REDIS_CACHE_ENABLED` | launch cache rehearsal: optional | `false` | Enables Redis-backed caches for hot search and recommendation candidates |
| `YOUYU_REDIS_HEALTH_ENABLED` | launch cache rehearsal: optional | `false` | Enables Actuator Redis health when Redis is required for a rehearsal |
| `REDIS_HOST` | launch cache rehearsal: optional | `localhost` | Redis host for the backend process |
| `REDIS_PORT` | launch cache rehearsal: optional | `6379` | Redis port for the backend process |
| `REDIS_PASSWORD` | launch cache rehearsal: optional | empty | Redis password, if configured |
| `REDIS_MAXMEMORY` | Compose Redis rehearsal: optional | `256mb` | Redis container memory ceiling for cache keys |
| `REDIS_MAXMEMORY_POLICY` | Compose Redis rehearsal: optional | `allkeys-lru` | Redis eviction policy for cache-only data |
| `YOUYU_REDIS_HOT_SEARCH_TTL` | launch cache rehearsal: optional | `1h` | Hot-search ranking cache TTL |
| `YOUYU_REDIS_HOME_RECOMMEND_TTL` | launch cache rehearsal: optional | `15m` | Home recommendation cache TTL |
| `YOUYU_REDIS_ALSO_BOUGHT_TTL` | launch cache rehearsal: optional | `15m` | Also-bought recommendation cache TTL |

**Production checklist**: `APP_JWT_SECRET` MUST be exported (≥32 chars) before booting any non-dev/seed/test profile. The application fails fast at startup if the committed dev default is detected under any other active profile.

Expand All @@ -39,6 +57,18 @@ provider credentials or network access. Enabling Kdniao only adds provider-backe
event lookup; Amap map rendering remains a separate map-provider concern and
must not be treated as live courier GPS.

Redis caching is disabled by default. When `YOUYU_REDIS_CACHE_ENABLED=true`, hot
search, home recommendation, and also-bought responses are cached with TTLs and
fall back to MySQL/JDBC paths if Redis reads or writes fail. Enable
`YOUYU_REDIS_HEALTH_ENABLED=true` only when Redis availability should affect
Actuator health for a specific staging or launch-performance rehearsal.

For staging or production-like rehearsals, set a non-empty `REDIS_PASSWORD`,
keep Redis private to the application network, and use a bounded memory policy
such as `REDIS_MAXMEMORY=256mb` with
`REDIS_MAXMEMORY_POLICY=allkeys-lru`. Redis currently stores cache-only data, so
MySQL remains the backup source of truth.

**First-time setup** (schema + seed data):
```bash
# Windows
Expand Down Expand Up @@ -77,6 +107,12 @@ mvnw.cmd clean package

## Seed data (profile `seed`)

Current demo-seed note:
- `application-seed.yml` now loads `seed/demo-expansion.sql` instead of the older `data-realistic-products.sql`.
- The expansion file provides the large demo catalog, fuller buyer lifecycles, denser admin dashboards, and support/message data used for the final demo overlay.
- Demo buyer login accounts remain `seedbuyer`-`seedbuyer4` / `user123` for compatibility, but their display-facing names and shops have been cleaned for presentation.
- For a fresh containerized demo reset, run `.\scripts\run-demo-fresh.ps1 -Detach`.

Loaded in order (see `application-seed.yml`):
1. `src/main/resources/schema.sql` — DDL (`CREATE TABLE IF NOT EXISTS`, idempotent)
2. `src/main/resources/seed/data.sql` — demo users, shops, categories, products
Expand Down
8 changes: 8 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.youyu.backend.common.cache;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.youyu.backend.config.RedisCacheProperties;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class RedisCacheSupport {

private static final Logger log = LoggerFactory.getLogger(RedisCacheSupport.class);
private static final TypeReference<List<Map<String, Object>>> LIST_OF_MAPS = new TypeReference<>() {
};

private final RedisCacheProperties properties;
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;

public RedisCacheSupport(RedisCacheProperties properties,
StringRedisTemplate redisTemplate,
ObjectMapper objectMapper) {
this.properties = properties;
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
}

public boolean isEnabled() {
return properties.isEnabled();
}

public Optional<List<Map<String, Object>>> getList(String key) {
if (!isEnabled()) {
return Optional.empty();
}
String namespacedKey = namespacedKey(key);
try {
String value = redisTemplate.opsForValue().get(namespacedKey);
if (value == null || value.isBlank()) {
return Optional.empty();
}
return Optional.of(objectMapper.readValue(value, LIST_OF_MAPS));
} catch (RuntimeException ex) {
log.warn("Redis cache read failed", ex);
return Optional.empty();
} catch (Exception ex) {
log.warn("Redis cache payload is unreadable", ex);
return Optional.empty();
}
}

public void putList(String key, List<Map<String, Object>> value, Duration ttl) {
if (!isEnabled() || value == null || ttl == null || ttl.isZero() || ttl.isNegative()) {
return;
}
String namespacedKey = namespacedKey(key);
try {
redisTemplate.opsForValue().set(namespacedKey, objectMapper.writeValueAsString(value), ttl);
} catch (RuntimeException ex) {
log.warn("Redis cache write failed", ex);
} catch (Exception ex) {
log.warn("Redis cache payload could not be serialized", ex);
}
}

public void evict(String key) {
if (!isEnabled()) {
return;
}
String namespacedKey = namespacedKey(key);
try {
redisTemplate.delete(namespacedKey);
} catch (RuntimeException ex) {
log.warn("Redis cache eviction failed", ex);
}
}

public void evictByPrefix(String prefix) {
if (!isEnabled()) {
return;
}
String namespacedPrefix = namespacedKey(prefix);
try {
Set<String> keys = redisTemplate.keys(namespacedPrefix + "*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
} catch (RuntimeException ex) {
log.warn("Redis cache prefix eviction failed for prefix={}", namespacedPrefix, ex);
}
}
Comment on lines +85 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using redisTemplate.keys() is a blocking O(N) operation that scans the entire keyspace of the Redis database. In production or high-traffic environments, this can block the single-threaded Redis event loop, leading to latency spikes, timeouts, and potential service degradation. Since this method is triggered on frequent operations like order completion, product updates, and review submissions, it should be refactored to use a non-blocking SCAN cursor.

    public void evictByPrefix(String prefix) {
        if (!isEnabled()) {
            return;
        }
        String namespacedPrefix = namespacedKey(prefix);
        try {
            redisTemplate.execute((org.springframework.data.redis.connection.RedisConnection connection) -> {
                try (org.springframework.data.redis.core.Cursor<byte[]> cursor = connection.scan(
                        org.springframework.data.redis.core.ScanOptions.scanOptions()
                                .match(namespacedPrefix + "*")
                                .count(1000)
                                .build())) {
                    while (cursor.hasNext()) {
                        connection.keyCommands().del(cursor.next());
                    }
                } catch (Exception e) {
                    log.warn("Error during Redis scan eviction", e);
                }
                return null;
            });
        } catch (RuntimeException ex) {
            log.warn("Redis cache prefix eviction failed for prefix={}", namespacedPrefix, ex);
        }
    }


private String namespacedKey(String key) {
return properties.getKeyPrefix() + key;
}
}
Loading
Loading