diff --git a/docker-compose.yml b/docker-compose.yml
index 5296c64..c3145ac 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -82,5 +82,28 @@ services:
user:
condition: service_started
+ # M3(ADR-017):社区服务(Feed/帖子/评论/互动)。骨架起 /api/v1/** 即接
+ # RS256 校验(与 user/pet 同一公钥);只读写 community schema。与 user 共库;
+ # Flyway 迁移链(V1..V5,含 community 基线)由 user 服务统一执行,故依赖
+ # user 先起,保证 community schema 已就绪。
+ community:
+ build: ./patbond-community
+ environment:
+ SPRING_CONFIG_LOCATION: file:/config/application.yml
+ PATBOND_DB_URL: jdbc:postgresql://postgres:5432/${PATBOND_DB_NAME:-patbond}
+ PATBOND_DB_USER: ${PATBOND_DB_USER:-patbond}
+ PATBOND_DB_PASSWORD: ${PATBOND_DB_PASSWORD:?先运行 deploy/init-secrets.sh 生成 .env}
+ PATBOND_JWT_PUBLIC_KEY: /run/patbond/keys/jwt-public.pem
+ volumes:
+ - ./patbond-community/src/main/resources/application.yml.sample:/config/application.yml:ro
+ - ./deploy/keys:/run/patbond/keys:ro
+ ports:
+ - "${PATBOND_COMMUNITY_PORT:-8084}:8084"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ user:
+ condition: service_started
+
volumes:
pgdata:
diff --git a/patbond-community/Dockerfile b/patbond-community/Dockerfile
new file mode 100644
index 0000000..8f55c3f
--- /dev/null
+++ b/patbond-community/Dockerfile
@@ -0,0 +1,9 @@
+# Runtime image only — build the jar first: ./mvnw -pl patbond-community -am package
+# Stateless by design (ADR-007): no local state, config via env / mounted files.
+FROM eclipse-temurin:17-jre
+RUN useradd --system --uid 10001 patbond
+USER patbond
+WORKDIR /app
+COPY target/patbond-community-1.0.0-SNAPSHOT-exec.jar app.jar
+EXPOSE 8084
+ENTRYPOINT ["java", "-jar", "/app/app.jar"]
diff --git a/patbond-community/pom.xml b/patbond-community/pom.xml
new file mode 100644
index 0000000..156907f
--- /dev/null
+++ b/patbond-community/pom.xml
@@ -0,0 +1,135 @@
+
+
+ 4.0.0
+
+
+ com.patbond.patbond
+ patbond-api
+ 1.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ patbond-community
+ jar
+
+ patbond-community
+ Community feed, posts and interactions service for Patbond (ADR-017)
+
+
+
+
+ com.patbond.patbond
+ patbond-common
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.6
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.6
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.6
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.patbond.patbond
+ patbond-user
+ ${project.version}
+ test
+
+
+ org.flywaydb
+ flyway-core
+ test
+
+
+ org.flywaydb
+ flyway-database-postgresql
+ test
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+ repackage
+
+
+
+ exec
+
+
+
+
+
+
+
diff --git a/patbond-community/src/main/java/com/patbond/patbond/community/CommunityApplication.java b/patbond-community/src/main/java/com/patbond/patbond/community/CommunityApplication.java
new file mode 100644
index 0000000..1168054
--- /dev/null
+++ b/patbond-community/src/main/java/com/patbond/patbond/community/CommunityApplication.java
@@ -0,0 +1,20 @@
+package com.patbond.patbond.community;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * Community feed, posts and interactions service (M3, ADR-017: the community
+ * domain lives in its own Maven module on :8084). First-wave skeleton:
+ * configuration wiring, datasource, RS256 bearer auth on /api/v1/** and a
+ * liveness endpoint — business endpoints follow the contract work in the
+ * next waves. The module only reads and writes the community schema
+ * (author profile lookups follow the D3-9 plan later).
+ */
+@SpringBootApplication
+public class CommunityApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(CommunityApplication.class, args);
+ }
+}
diff --git a/patbond-community/src/main/java/com/patbond/patbond/community/config/CommunitySecurityProperties.java b/patbond-community/src/main/java/com/patbond/patbond/community/config/CommunitySecurityProperties.java
new file mode 100644
index 0000000..5e5dea4
--- /dev/null
+++ b/patbond-community/src/main/java/com/patbond/patbond/community/config/CommunitySecurityProperties.java
@@ -0,0 +1,37 @@
+package com.patbond.patbond.community.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Security knobs of the community service: only the RS256 public key for
+ * verifying access tokens issued by patbond-auth (same contract as
+ * patbond-user/pet's {@code patbond.jwt.public-key}). No /internal routes
+ * exist here yet, so no service token property.
+ */
+@ConfigurationProperties(prefix = "patbond")
+public class CommunitySecurityProperties {
+
+ private final Jwt jwt = new Jwt();
+
+ public Jwt getJwt() {
+ return jwt;
+ }
+
+ public static class Jwt {
+
+ /**
+ * RS256 public key for verifying access tokens signed by
+ * patbond-auth: either inline PEM (starts with -----BEGIN) or a
+ * filesystem path. The private key never reaches this service.
+ */
+ private String publicKey;
+
+ public String getPublicKey() {
+ return publicKey;
+ }
+
+ public void setPublicKey(String publicKey) {
+ this.publicKey = publicKey;
+ }
+ }
+}
diff --git a/patbond-community/src/main/java/com/patbond/patbond/community/config/JacksonConfig.java b/patbond-community/src/main/java/com/patbond/patbond/community/config/JacksonConfig.java
new file mode 100644
index 0000000..e49092e
--- /dev/null
+++ b/patbond-community/src/main/java/com/patbond/patbond/community/config/JacksonConfig.java
@@ -0,0 +1,21 @@
+package com.patbond.patbond.community.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Integer fields must reject decimal input (development-plan 4.3) — Jackson's
+ * default is to silently truncate 45.5 to 45 in an integer field, which
+ * would corrupt values instead of rejecting them. Same setting as the other
+ * services so all envelopes behave identically.
+ */
+@Configuration
+public class JacksonConfig {
+
+ @Bean
+ public Jackson2ObjectMapperBuilderCustomizer rejectFloatAsInt() {
+ return builder -> builder.featuresToDisable(DeserializationFeature.ACCEPT_FLOAT_AS_INT);
+ }
+}
diff --git a/patbond-community/src/main/java/com/patbond/patbond/community/config/SecurityConfig.java b/patbond-community/src/main/java/com/patbond/patbond/community/config/SecurityConfig.java
new file mode 100644
index 0000000..50e327a
--- /dev/null
+++ b/patbond-community/src/main/java/com/patbond/patbond/community/config/SecurityConfig.java
@@ -0,0 +1,35 @@
+package com.patbond.patbond.community.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.patbond.patbond.community.security.BearerAuthFilter;
+import com.patbond.patbond.community.security.JwtVerifier;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Wires bearer authentication for /api/v1/** without pulling in
+ * spring-security — the same single-filter pattern patbond-user and
+ * patbond-pet use. The /health probe stays outside /api/v1 and therefore
+ * unauthenticated.
+ */
+@Configuration
+@EnableConfigurationProperties(CommunitySecurityProperties.class)
+public class SecurityConfig {
+
+ @Bean
+ public JwtVerifier jwtVerifier(CommunitySecurityProperties properties) {
+ return new JwtVerifier(properties.getJwt().getPublicKey());
+ }
+
+ @Bean
+ public FilterRegistrationBean bearerAuthFilter(
+ JwtVerifier jwtVerifier, ObjectMapper objectMapper) {
+ FilterRegistrationBean registration = new FilterRegistrationBean<>(
+ new BearerAuthFilter(jwtVerifier, objectMapper));
+ registration.addUrlPatterns("/api/v1/*");
+ registration.setOrder(20);
+ return registration;
+ }
+}
diff --git a/patbond-community/src/main/java/com/patbond/patbond/community/controller/HealthController.java b/patbond-community/src/main/java/com/patbond/patbond/community/controller/HealthController.java
new file mode 100644
index 0000000..b7ead10
--- /dev/null
+++ b/patbond-community/src/main/java/com/patbond/patbond/community/controller/HealthController.java
@@ -0,0 +1,36 @@
+package com.patbond.patbond.community.controller;
+
+import com.patbond.patbond.common.response.ApiResponse;
+import org.springframework.jdbc.core.simple.JdbcClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ * Liveness/readiness probe for the skeleton phase: confirms the service is
+ * up and that its datasource can reach the shared PostgreSQL. Deliberately
+ * outside /api/v1 so it stays unauthenticated (same reasoning as compose's
+ * pg_isready: infrastructure probes carry no business data).
+ */
+@RestController
+public class HealthController {
+
+ private final JdbcClient jdbcClient;
+
+ public HealthController(JdbcClient jdbcClient) {
+ this.jdbcClient = jdbcClient;
+ }
+
+ @GetMapping("/health")
+ public ApiResponse