From aa633b8c0235cddc59850de5f6dc700bad33da39 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 12 十月 2026 15:54:22 +0800
Subject: [PATCH] chore(merge): 合并远端更新(臻享礼遇商家类型回填、小程序页面)

---
 rencaika/config/api.uts                                                                                    |    4 
 rencaika/pages/index/index.uvue                                                                            |    5 
 rencaika/docs/pages/59-首页.md                                                                               |    2 
 talent-card-server/src/test/java/com/talentcard/server/business/admin/PremiumServiceMerchantTypeTests.java |  132 +++++++++++++++++++++++++++++++++
 talent-card-server/src/main/resources/db/migration/V157__backfill_premium_service_merchant_types.sql       |   15 +++
 .scratch/server-testenv/README.md                                                                          |    8 ++
 rencaika/pages.json                                                                                        |   15 +++
 7 files changed, 176 insertions(+), 5 deletions(-)

diff --git a/.scratch/server-testenv/README.md b/.scratch/server-testenv/README.md
index 5746d51..358ab3b 100644
--- a/.scratch/server-testenv/README.md
+++ b/.scratch/server-testenv/README.md
@@ -20,6 +20,14 @@
 | `/data/mysql` | MySQL datadir(从 `/var/lib/mysql` 迁入,AppArmor 走 `local/usr.sbin.mysqld` 覆盖) |
 | `/var/lib/mysql` | 迁移前的原始副本,**回滚用**,确认稳定后可删 |
 
+## 首页顶部图更新(2026-10-10 16:16)
+
+- 按用户提供的图片替换线上 `/data/talent-card/uploads/home/home-top.png`,保留小程序现有圆角和布局。
+- 新图 222141 字节,SHA256:`496e4cd33b81ebe89b6d382c3b87f9dfd0964e225c00e2e489137707e1269aa9`。
+- 旧图备份:`/data/talent-card/backups/home-images/home-top-20261010-161619.png`。
+- 使用后台图片上传接口传输,退出此次上传会话;服务器校验后原子替换。公网原地址返回 200,下载文件 SHA256 与附件一致。没有重启后端。
+- 本地开发环境的旧图保留;本次仅更新线上资源。客户端若缓存旧图,需要重新加载或清除图片缓存。
+
 ## 环境变量(`/etc/talent-card/env`,10 个键)
 
 **改配置只改这个文件**,不要在单元里写 `Environment=` —— systemd 里 `EnvironmentFile` 会覆盖同名
diff --git a/rencaika/config/api.uts b/rencaika/config/api.uts
index 2066371..3d16db5 100644
--- a/rencaika/config/api.uts
+++ b/rencaika/config/api.uts
@@ -4,8 +4,8 @@
 import { developmentApiBaseUrl } from './development-server.uts'
 
 // 2026-09-29:体验版指向攀枝花测试环境。本机联调改回 'development';生产上线改 'production'。
-// 2026-10-09:本地联调电影票,临时切 'development'(开发者工具走 127.0.0.1:8080,见 config/development-server.uts)。测完改回 'staging'。
-export const apiEnvironment: ApiEnvironment = 'development'
+// 2026-10-10:切回政务云已部署环境,接口和上传图片统一使用 stagingBaseUrl。
+export const apiEnvironment: ApiEnvironment = 'staging'
 // Separate from demo-data controls; only development builds request a test session.
 export const developmentLoginBypassEnabled: boolean = false 
 
diff --git "a/rencaika/docs/pages/59-\351\246\226\351\241\265.md" "b/rencaika/docs/pages/59-\351\246\226\351\241\265.md"
index a208002..f4a4205 100644
--- "a/rencaika/docs/pages/59-\351\246\226\351\241\265.md"
+++ "b/rencaika/docs/pages/59-\351\246\226\351\241\265.md"
@@ -14,7 +14,7 @@
 | --- | --- | --- |
 | 顶部导航 | 按微信胶囊动态适配;标题是文本「攀枝花市东区人才服务平台」,用行楷(优先服务器字体 `FoodCalligraphyFull`,加载失败或缺字回退系统字体);下滑超过阈值后橙色实体背景渐显,标题位置保持固定。 | 后台无需下发导航资源。 |
 | 东区主图 | 主图取服务器 `/uploads/home/home-top.png`(`homeImage()` 拼当前接口域名),上半部压橙色透明渐变、底部圆角,提供“向东看”入口。 | 后台返回宣传视频或内容链接及封面。 |
-| 城市介绍 | 展示东区简介、潜力城、兴业城、黄金城、宜居城入口,配图取服务器 `/uploads/home/city-*.jpg\|png`;换图只换服务器文件,不用发版。 | 后台配置专题类型、标题、图片和详情内容。 |
+| 城市介绍 | 东区简介进入独立分包图文页,展示潜力城、产业城、黄金城、宜居城四张宣传图,支持点击放大;“幸福东区”仍跳后台配置的小程序或网页。首页卡片配图取服务器 `/uploads/home/city-*.jpg\|png`。 | 简介内容见 `东区简介.md`,随小程序发布;首页卡片换图只换服务器文件。 |
 | 人才资讯 | 配置服务地址后调用 `GET /api/app/home` 获取置顶及普通资讯摘要,按稳定 ID 进入富文本资讯详情;查看更多进入资讯列表。 | 后台发布管理、封面图和附件待补充。 |
 | 底部导航 | 首页、人才权益、人才卡、人才政策、个人中心为平级页面,使用 `reLaunch` 切换。 | 登录态失效时统一进入登录页。 |
 
diff --git a/rencaika/pages.json b/rencaika/pages.json
index 66b9450..44f699c 100644
--- a/rencaika/pages.json
+++ b/rencaika/pages.json
@@ -492,6 +492,21 @@
 			}
 		}
 	],
+	"subPackages": [
+		{
+			"root": "city-intro",
+			"pages": [
+				{
+					"path": "pages/intro/intro",
+					"style": {
+						"navigationStyle": "default",
+						"navigationBarTitleText": "东区简介",
+						"backgroundColor": "#FFFFFF"
+					}
+				}
+			]
+		}
+	],
 	"globalStyle": {
 		"navigationBarTextStyle": "black",
 		"navigationBarTitleText": "东区人才卡",
diff --git a/rencaika/pages/index/index.uvue b/rencaika/pages/index/index.uvue
index 1e71138..54f470d 100644
--- a/rencaika/pages/index/index.uvue
+++ b/rencaika/pages/index/index.uvue
@@ -12,7 +12,7 @@
 
 				<view class="city-panel">
 					<view class="city-panel-content">
-						<view class="city-heading"><text>才聚东区,共富未来</text><view class="city-label" @tap="openIntro"><text>幸福东区</text><image src="/static/home-assets/arrow-right.svg"></image></view></view>
+						<view class="city-heading"><text>才聚东区,共富未来</text><view class="city-label" @tap="openHappyDongqu"><text>幸福东区</text><image src="/static/home-assets/arrow-right.svg"></image></view></view>
 						<view class="city-grid">
 							<view class="city-card city-intro" @tap="openIntro"><image class="city-bg" :src="homeImage('city-intro.jpg')" mode="aspectFill"></image><text class="intro-name">东区简介</text><view class="intro-button"><text>查看更多</text><image src="/static/home-assets/arrow-right.svg"></image></view></view>
 							<view class="city-side"><view class="city-card city-potential"><image class="city-bg" :src="homeImage('city-potential.jpg')" mode="aspectFill"></image><view class="city-card-caption"></view><text>潜力城</text></view><view class="city-card city-golden"><image class="city-bg" :src="homeImage('city-golden.jpg')" mode="aspectFill"></image><view class="city-card-caption"></view><text>黄金城</text></view></view>
@@ -137,7 +137,8 @@
 		}
 		uni.navigateTo({ url: '/pages/home-video/home-video?url=' + encodeURIComponent(resolveApiUrl(homeVideoUrl.value)) })
 	}
-	const openIntro = () => {
+	const openIntro = () => uni.navigateTo({ url: '/city-intro/pages/intro/intro' })
+	const openHappyDongqu = () => {
 		if (miniProgramAppId.value != '') {
 			uni.navigateToMiniProgram({
 				appId: miniProgramAppId.value,
diff --git a/talent-card-server/src/main/resources/db/migration/V157__backfill_premium_service_merchant_types.sql b/talent-card-server/src/main/resources/db/migration/V157__backfill_premium_service_merchant_types.sql
new file mode 100644
index 0000000..7250549
--- /dev/null
+++ b/talent-card-server/src/main/resources/db/migration/V157__backfill_premium_service_merchant_types.sql
@@ -0,0 +1,15 @@
+-- V48 used the generic service type; V119 consequently classified these rows as other.
+-- Restore both fields from the active premium mapping without overwriting explicit types.
+-- The unique merchant_id key on premium_topic_merchant makes the mapping unambiguous.
+update merchant m
+join premium_topic_merchant p on p.merchant_id = m.id and p.deleted = 0
+set m.merchant_type_code = p.category_key,
+    m.merchant_type = case p.category_key
+        when 'children' then '子女托管'
+        when 'approval' then '行政审批'
+        when 'employment' then '配偶推荐就业'
+    end
+where m.deleted = 0
+  and m.merchant_type = 'service'
+  and m.merchant_type_code = 'other'
+  and p.category_key in ('children', 'approval', 'employment');
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/admin/PremiumServiceMerchantTypeTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/admin/PremiumServiceMerchantTypeTests.java
new file mode 100644
index 0000000..372ffa6
--- /dev/null
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/admin/PremiumServiceMerchantTypeTests.java
@@ -0,0 +1,132 @@
+package com.talentcard.server.business.admin;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+
+@SpringBootTest
+@ActiveProfiles("test")
+@Transactional
+class PremiumServiceMerchantTypeTests {
+
+    @Autowired
+    private JdbcTemplate jdbc;
+
+    @Autowired
+    private AdminBusinessService service;
+
+    @ParameterizedTest
+    @CsvSource({
+            "children, 子女托管, CHILD_ENTRUSTMENT",
+            "approval, 行政审批, ADMIN_APPROVAL",
+            "employment, 配偶推荐就业, SPOUSE_EMPLOYMENT"
+    })
+    void migratedTypeAgreesAcrossListFiltersDetailAndEditing(String code, String label, String businessType)
+            throws Exception {
+        long id = insertMerchant("service", "other", 0);
+        insertMapping(id, code, businessType, 0);
+
+        migrate();
+        Map<String, Object> detail = service.merchant(id);
+        assertThat(detail).containsEntry("merchantType", label).containsEntry("merchantTypeCode", code);
+        String name = (String) detail.get("merchantName");
+        assertList(id, name, null, code, label, 1);
+        assertList(id, name, label, code, label, 1);
+        assertList(id, name, code, code, label, 1);
+        assertList(id, name, "food", code, label, 0);
+
+        // The edit page submits the display name returned by the detail endpoint.
+        Map<String, Object> saved = service.updateMerchant(id, new MerchantUpdateRequest(
+                name + " edited", (String) detail.get("merchantType"), "Test contact", "13800000000",
+                "Test address", null, null, "营业中", (byte) 0, 0, (byte) 1,
+                null, null, (byte) 0, null, null, null, null, null, null, null, (byte) 0, null, null));
+        assertThat(saved).containsEntry("merchantType", label).containsEntry("merchantTypeCode", code);
+        assertThat(jdbc.queryForMap(
+                "select category_key, business_type, deleted from premium_topic_merchant where merchant_id = ?", id))
+                .containsEntry("category_key", code).containsEntry("business_type", businessType);
+        assertThat(jdbc.queryForObject(
+                "select deleted from premium_topic_merchant where merchant_id = ?", Integer.class, id)).isZero();
+        assertList(id, name + " edited", label, code, label, 1);
+
+        // Reapplying the repair must also preserve a subsequent explicit type edit.
+        jdbc.update("update merchant set merchant_type = '餐饮服务', merchant_type_code = 'food' where id = ?", id);
+        migrate();
+        assertThat(service.merchant(id))
+                .containsEntry("merchantType", "餐饮服务").containsEntry("merchantTypeCode", "food");
+    }
+
+    @Test
+    void repairIsIdempotentAndLeavesUnrelatedOrDeletedRecordsUntouched() throws Exception {
+        long fixed = insertMerchant("service", "other", 0);
+        insertMapping(fixed, "children", "CHILD_ENTRUSTMENT", 0);
+        long unmapped = insertMerchant("service", "other", 0);
+        long deletedMerchant = insertMerchant("service", "other", 1);
+        insertMapping(deletedMerchant, "children", "CHILD_ENTRUSTMENT", 0);
+        long deletedMapping = insertMerchant("service", "other", 0);
+        insertMapping(deletedMapping, "approval", "ADMIN_APPROVAL", 1);
+        long unsupportedMapping = insertMerchant("service", "other", 0);
+        insertMapping(unsupportedMapping, "finance", "FINANCE", 0);
+        long explicitCode = insertMerchant("service", "food", 0);
+        insertMapping(explicitCode, "employment", "SPOUSE_EMPLOYMENT", 0);
+        long explicitName = insertMerchant("LOCAL_TEST", "other", 0);
+        insertMapping(explicitName, "children", "CHILD_ENTRUSTMENT", 0);
+
+        migrate();
+        Map<String, Object> repaired = jdbc.queryForMap("select * from merchant where id = ?", fixed);
+        migrate();
+        assertThat(jdbc.queryForMap("select * from merchant where id = ?", fixed)).isEqualTo(repaired);
+        assertType(fixed, "子女托管", "children");
+        for (long id : new long[]{unmapped, deletedMerchant, deletedMapping, unsupportedMapping}) {
+            assertType(id, "service", "other");
+        }
+        assertType(explicitCode, "service", "food");
+        assertType(explicitName, "LOCAL_TEST", "other");
+    }
+
+    private void assertList(long id, String name, String filter, String code, String label, int expected) {
+        var page = service.merchants(name, null, filter, null, null, null, null, 1, 10);
+        assertThat(page.total()).isEqualTo(expected);
+        assertThat(page.records()).hasSize(expected);
+        if (expected == 1) {
+            assertThat(page.records().get(0))
+                    .containsEntry("id", id).containsEntry("merchantType", label).containsEntry("merchantTypeCode", code);
+        }
+    }
+
+    private void assertType(long id, String label, String code) {
+        assertThat(jdbc.queryForMap("select merchant_type, merchant_type_code from merchant where id = ?", id))
+                .containsEntry("merchant_type", label).containsEntry("merchant_type_code", code);
+    }
+
+    private long insertMerchant(String label, String code, int deleted) {
+        jdbc.update("""
+                insert into merchant(merchant_name, merchant_type, merchant_type_code, address, deleted)
+                values (?, ?, ?, 'Test address', ?)
+                """, "premium-type-" + UUID.randomUUID(), label, code, deleted);
+        return jdbc.queryForObject("select last_insert_id()", Long.class);
+    }
+
+    private void insertMapping(long id, String category, String businessType, int deleted) {
+        jdbc.update("""
+                insert into premium_topic_merchant(merchant_id, category_key, business_type, deleted)
+                values (?, ?, ?, ?)
+                """, id, category, businessType, deleted);
+    }
+
+    private void migrate() throws Exception {
+        jdbc.execute(new ClassPathResource("db/migration/V157__backfill_premium_service_merchant_types.sql")
+                .getContentAsString(StandardCharsets.UTF_8));
+    }
+}

--
Gitblit v1.7.1