From 922823a3f1faaf0293256e3ffb1285eaeb8a3b23 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期日, 04 十月 2026 07:03:42 +0800
Subject: [PATCH] fix(holiday): 快报百分比按Excel显示精度取值

---
 HANDOFF.md                                                                                          |   24 +++++
 AGENTS.md                                                                                           |    4 +
 traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayBulletinServiceTest.java |   28 ++++--
 traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java     |  134 +++++++++++++++++++++------------
 4 files changed, 130 insertions(+), 60 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index b497aa7..03ec2eb 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -34,6 +34,10 @@
 - 娌欑鍐� `Get-NetTCPConnection` 浼氬洜鏉冮檺鈥滄嫆缁濊闂�濊�岄潤榛樺け璐ワ紝**涓嶈**鎹鍒ゅ畾绔彛鏈洃鍚紱鍒ゆ柇鏈嶅姟鐘舵�佷互 backend.log / frontend.log 涓哄噯銆�
 - 鐢ㄦ埛瑕佹眰锛氫复鏃惰剼鏈�/鏂囦欢鏈粡鍚屾剰涓嶅垹锛涘鍏ュけ璐ヨ鍦ㄩ〉闈㈡樉绀鸿鍙凤紱AI 鍒嗘瀽缁撴灉瑕佺粰缁撴瀯鍖栨帓鐗� + 閫愬瓧杈撳嚭鍔ㄧ敾銆�
 
+## 閲嶈鑺傚亣鏃ュ揩鎶ヤ笌鍒嗘瀽鎶ュ憡鍙e緞锛�2026-10-04 璧凤級
+- 蹇姤鐧惧垎姣斿繀椤诲厛鎸夊鍏ョ殑 `2026骞村崄涓�鏁版嵁.xlsx` 瀵瑰簲鍗曞厓鏍肩殑鏁板瓧鏍煎紡鐢熸垚鏄剧ず鍊硷紝鍐嶆寜蹇姤缁熶竴 1 浣嶅皬鏁板彇鏁淬�傚綋鍓嶆簮琛ㄤ笉鏄叏灞�鍚屼竴绮惧害锛氬鏁板崟鍏冩牸鏄� `0.0%`锛沗浜哄憳娴佸姩閲忔眹鎬籤 鐨勫綋鏃ョ幆姣� D/P/J銆佺疮璁″悓姣� V/W/Y/Z/AB/AC锛屼互鍙婃按璺綋鏃ョ幆姣� D 鏄� `0.00%`銆�
+- 鏍稿棰嗗鍒嗘瀽鎶ュ憡鏃讹紝鎸夎鍒嗘瀽鎶ュ憡鑷韩鏄剧ず鐨勫皬鏁颁綅鏍稿锛屼笉濂楃敤蹇姤鐨� 1 浣嶅皬鏁板彛寰勶紱鐩爣鏄鐜颁汉宸ュ湪鏂囨。/Excel 涓婄湅鍒扮殑鍊笺��
+
 ## 宸ヤ綔瑙勮寖锛�2026-09-02 璧凤紱2026-09-27 鎸夊悓浜嬫彁浜ゆ柟寮忚皟鏁达級
 
 1. **姣忓仛瀹屼竴涓姛鑳斤紝绔嬪嵆娴嬭瘯**锛堟祴璇曡姹備笉鍙橈級
diff --git a/HANDOFF.md b/HANDOFF.md
index 2bcd665..ee3c765 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -4,7 +4,28 @@
 > **姣忓ぉ缁撴潫鏃�**锛氭妸褰撳ぉ杩涘睍銆佽俯杩囩殑鍧戙�佹柊闇�姹傘�佹湭瀹屾垚浜嬮」鏇存柊鍒版湰鏂囦欢锛岀劧鍚庡彲浠ユ斁蹇冨紑鏂板璇濄��
 > **鍗忎綔绾﹀畾锛�2026-08-31锛�**锛氳銆屾彁浜ゃ��= 鏈湴 commit + 鎺ㄩ�佸埌杩滅 git锛坥rigin main锛孏itblit锛夛紝涓嶅啀鍙仛鏈湴 commit銆�
 
+## 鏈�鏂帮紙2026-10-04锛�
+
+### 10-04锛氬揩鎶ョ櫨鍒嗘瘮鎸� Excel 鍗曞厓鏍兼樉绀虹簿搴﹀悗鍐嶅彇 1 浣�
+
+- **鐢ㄦ埛纭鐨勪笟鍔″彛寰�**锛氫汉宸ユ牳瀵逛互 `2026骞村崄涓�鏁版嵁.xlsx` 鍗曞厓鏍煎疄闄呮樉绀哄�间负鍑嗐�傛瘮濡傛�昏绱鍚屾瘮 2025 搴曞眰绮剧‘鍊间负 `-4.448602%`锛孍xcel 鍗曞厓鏍兼牸寮忎负 `0.00%`锛屼汉鐪嬪埌骞剁敤浜庢牳瀵圭殑鏄� `-4.45%`锛屽啀鍙栧揩鎶� 1 浣嶅皬鏁板氨鏄� `-4.5%`銆傛鍓嶁�滅洿鎺ユ寜搴曞眰鍊煎彇涓�浣嶅緱鍒� -4.4%鈥濈殑缁撹宸蹭綔搴熴��
+- **瀹炵幇**锛歚HolidayBulletinService` 鏂板 `bulletinPercent(...)`锛屼弗鏍兼寜鐧惧垎姣旀潵婧愬崟鍏冩牸鏍煎紡鍏堝彇鏄剧ず绮惧害锛屽啀鍙栧揩鎶� 1 浣嶅皬鏁帮紱姝f枃鎺緸涓庤〃鏍肩櫨鍒嗘瘮鍏辩敤鍚屼竴閫昏緫銆傚綋鍓嶆簮琛ㄩ�愬垪鍙e緞锛氬鏁版簮鍗曞厓鏍间负 `0.0%`锛沗浜哄憳娴佸姩閲忔眹鎬籤 鐨勫綋鏃ョ幆姣� D/P/J銆佺疮璁″悓姣� V/W/Y/Z/AB/AC锛屼互鍙婃按璺綋鏃ョ幆姣� D 涓� `0.00%`銆�
+- **棰嗗鍒嗘瀽鎶ュ憡鏍稿瑙勫垯**锛氶瀵煎垎鏋愭姤鍛婁笉濂楃敤蹇姤鐨� 1 浣嶅皬鏁帮紝涓�寰嬫寜璇ユ姤鍛婅嚜韬樉绀虹殑灏忔暟浣嶆牳瀵癸紝纭繚涓庝汉宸ュ湪鏂囨。閲岀湅鍒扮殑鍊间竴鑷淬��
+- **楠岃瘉**锛氭柊澧�/鏇存柊鍗曞厓娴嬭瘯瑕嗙洊 `-4.448602%` 鐨� `-4.4%`锛�1 浣嶆簮鏍煎紡锛変笌 `-4.5%`锛�2 浣嶆簮鏍煎紡锛夛紝浠ュ強 `-0.045% -> -0.05% -> -0.1%` 杈圭晫锛涘悗绔� `mvn test` 鍏ㄩ儴閫氳繃銆傞噸鍚� 8090 鍚庣敤鐪熷疄搴撴暟鎹敓鎴� 2026-10-02 蹇姤锛屾鏂囧拰琛ㄦ牸涓庡凡浜哄伐鏍稿鐨勬ā鏉块�愰」涓�鑷达紝鎬昏绱鍚屾瘮 2025/2024 涓� `-4.5% / -1.6%`銆�
+- **鏈湴楠岃瘉浜х墿**锛歚_tmp_holiday_verify/bulletin_excel_display_rounding_2026-10-02.docx`锛涘姛鑳芥祴璇曟姤鍛婁笌宸ヤ綔鏃ョ粨鎸夋湰鍦扮暀妗h鍒欏啓鍏� `docs/鍔熻兘娴嬭瘯鎶ュ憡/`銆乣docs/宸ヤ綔鏃ョ粨/`锛屼笉鍏ュ簱銆�
+- **鐘舵��**锛氫唬鐮佸拰鍙e緞璇存槑闅忔湰娆℃彁浜ゆ帹閫侊紱鏈嶅姟淇濇寔杩愯锛岃闂� `http://localhost:8080`銆�
+
 ## 鏈�鏂帮紙2026-10-03锛�
+
+### 銆愭柊瀵硅瘽鍏堣銆戝綋鍓嶇姸鎬佷笌涓嬩竴姝ワ紙2026-10-03 娣卞鍐欏叆锛�
+
+- **浠g爜鐘舵�侊紙宸叉帹閫侊級**锛歚HEAD = origin/main = f329f50`锛屽唴瀹逛负 `feat(observation): 鏂板瑙傛祴绔欏垎鏃ヨ皟鏌ユ暟鎹鍏ヤ笌鏄庣粏鏌ョ湅`锛岀敤鎴峰凡銆岄獙鏀堕�氳繃銆嶃�傚伐浣滃尯闄ゆ湰 HANDOFF.md 鐨勪氦鎺ユ洿鏂帮紙灏氭湭鎻愪氦锛岀瓑鐢ㄦ埛纭锛夊锛屾棤鍏朵粬宸茶窡韪枃浠舵敼鍔紱浠呭墿鏈窡韪殑 `_tmp_doc_compare/`銆乣_tmp_holiday_verify/` 鍜� `docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/瑙傛祴绔欏垎鏃ヨ皟鏌ユ暟鎹� (2026骞�10鏈�1鏃�).xls`锛堣繃绋嬩骇鐗╀笌婧愭暟鎹紝鎸夌害瀹氫繚鐣欍�佷笉鍏ュ簱銆佷笉鍒犻櫎锛夈�俙docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/2026骞村崄涓�鏁版嵁.xlsx` 宸茬敱鐢ㄦ埛鏀瑰洖鍘熷悕锛屾ā鏉夸笅杞芥帴鍙e疄娴� HTTP 200 / 36335 瀛楄妭銆�
+- **鏈嶅姟鐘舵��**锛氬悗绔� 8090銆佸墠绔� 8080 鍧囧湪杩愯锛岃闂� `http://localhost:8080`锛涚櫥褰� admin/admin123銆傛暟鎹簱 `127.0.0.1:3305/traffic_audit`锛坮oot/root锛夈��
+- **搴撳唴鏁版嵁**锛氳妭鍋囨棩 2026-09-30锛堥搧璺�/姘戣埅鍓嶄竴鏃ワ級銆�2026-10-01銆�2026-10-02 宸插鍏ワ紱**2026-10-03 灏氭湭瀵煎叆**銆傝娴嬬珯 `observation_station_flow` 鐜版湁 2026-10-01锛�692 绔欙級涓� 2026-10-02锛�689 绔欙級銆�
+- **涓嬩竴姝ワ紙2026-10-04 鏃╀笂锛�**锛�10 鏈� 3 鏃ユ暟鎹嚭鏉ュ悗锛屾寜鏃㈡湁鍙e緞瀵煎叆 2026-10-03 骞剁敓鎴愬綋澶┿�屾箹鍖楃渷鍥藉簡鍋囨湡缁煎悎杩愯緭瀹㈡祦蹇姤銆嶃�傞瀵艰姹� 8:30 鍓嶅嚭绗竴鐗堬紝鑰屾皯鑸暟鎹彲鑳芥櫄浜� 8:30 鎵嶆湁锛氭皯鑸湭鍒板氨鍏堝嚭缂烘皯鑸増鏈紙绱鍚屾瘮鍙笌寰�骞村凡瑕嗙洊鏃ユ湡姣旇緝锛夛紝姘戣埅鍒伴綈鍚庨噸鏂板鍏ュ苟閲嶆柊鐢熸垚銆傚畬鏁村彛寰勪笌宸查獙璇佺粨璁鸿鏈枃浠� 10-03 鍚勬銆�
+- **鏃犺嚜鍔ㄥ寲**锛氭浘寤虹殑蹇冭烦鎻愰啋锛坕d `10-03`锛夊凡鎸夌敤鎴疯姹傚垹闄わ紝鐢ㄦ埛 2026-10-04 鏄庣‘銆屽埌鏃舵垜浼氭墜宸ユ搷浣溿�嶃�傛槑澶╂棭涓婄敱鐢ㄦ埛鎵嬪伐瀵煎叆 10-03 鏁版嵁骞剁敓鎴愬揩鎶ワ紝鍔╂墜鍙湪鍏舵彁鍑烘椂閰嶅悎鏍稿/鎺掓煡銆備笉瑕佸湪鏈幏鐢ㄦ埛鏄庣‘瑕佹眰鏃惰嚜琛屽垱寤哄畾鏃朵换鍔℃垨鑷姩鍐欏簱銆�
+- **鏁版嵁鍙戠幇锛堜緵鍙傝�冿紝鏃犻渶鏀逛唬鐮侊級**锛�2026-10-02 姣� 10-01 灏� 3 涓娴嬬珯鈥斺�擿G348L149420501` 鍦熼棬锛堝疁鏄屽競/甯傝緰鍖猴級銆乣S464L105422828` 澶у吀娌筹紙鎭╂柦宸�/楣ゅ嘲鍘匡級銆乣S483L105420801` 灞堝宀紙鑽嗛棬甯�/甯傝緰鍖猴級銆傚凡鏍稿涓や唤婧� Excel锛岀‘灞炴簮鏁版嵁褰撳ぉ鏈笂鎶ワ紝涓嶆槸瀵煎叆婕忚銆�
+- **闀挎湡寰呭姙**锛氳涓嬫柟绗竷鑺傘�屾暟鍊肩簿搴︾粺涓�鏀归�犮�嶏紙鏃㈡湁 16 寮犺〃銆�228 涓� double 鍒楁敼 `DECIMAL(30,15)` + Java `BigDecimal`锛夈��
 
 ### 10-03 鏅氾細閲嶈鑺傚亣鏃ユ柊妯℃澘锛�2019/鍓嶄竴鏃ワ級閫傞厤
 - **妯℃澘鍙樺寲**锛歚2026骞村崄涓�鏁版嵁.xlsx` 鐨勯亾璺〃澧炲姞 2019 骞村強鍚屾瘮2019锛涢搧璺�佹皯鑸鍔犫�滃墠涓�鏃モ�濊锛涘揩鎶ユā鏉垮鍔犲悓姣�2024 鍒椼�傛棫妯℃澘宸叉斁鍦� `docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/澶囦唤/`銆�
@@ -26,6 +47,7 @@
 ### 10-03 娣卞锛�2锛夛細缂哄綋鏃ユ皯鑸暟鎹椂鐨勭疮璁″悓姣斿姣旀湡闂翠慨姝�
 - **蹇姤姘戣埅绱鍚屾瘮瀵规瘮鏈熼棿淇锛�2026-10-03锛岀己褰撴棩姘戣埅鏁版嵁鍦烘櫙锛�**锛氬綋鏌愯繍杈撴柟寮忓綋骞寸疮璁¤鐩栫殑鏃ユ湡鐭簬鎵�閫夋棩鏈燂紙鍏稿瀷涓� 10 鏈� 2 鏃ユ皯鑸暟鎹湭鍑恒�佹皯鑸疮璁″彧鏈� 10-01锛夛紝绱鍚屾瘮鏀逛负銆屽彧鍜屽線骞村凡缁忚鐩栧埌鐨勬棩鏈熸瘮杈冦�嶁�斺�斿厛鎵惧綋骞存渶鍚庝竴涓湁璇ユ寚鏍囨暟鎹殑鏃ユ湡锛屽啀鍙栧線骞村悓鏃ョ疮璁°�備慨澶嶅悗缂哄け姘戣埅鐨� 10 鏈� 2 鏃ュ揩鎶ユ皯鑸疮璁″悓姣旂敱 `-50.5%/-49.9%` 鍙樹负 `-10.0%/-0.9%`锛堜笌鎵嬪伐绋� -10.03%/-0.87% 鐨� 1 浣嶅皬鏁板�间竴鑷达級銆傚疄鐜拌 `HolidayBulletinService.comparableCumulative`锛堜粎浣滅敤浜庢皯鑸锛夛紝鍏朵綑琛屼笌鎬讳汉鍛樻祦鍔ㄩ噺鍙e緞涓嶅彉銆�
 - **銆岀疮璁″悓姣� -4.65% vs 蹇姤 -4.6%銆嶇粨璁猴紙2026-10-03锛屾棤闇�鏀逛唬鐮侊級**锛欵xcel 鏄剧ず鐨� -4.65% 鏄�**涓や綅灏忔暟**缁撴灉锛涘簳灞傜簿纭瘮鍊兼槸 `-4.64633%`锛�2026 绱 3082.23818銆�2025 绱 3232.42742锛夛紝**涓�娆″洓鑸嶄簲鍏ュ埌 1 浣嶅皬鏁板氨鏄� -4.6%**銆傛妸宸茬粡鍙栧埌涓や綅灏忔暟鐨� -4.65 鍐嶅彇涓�浣嶆墠寰楀埌 -4.7锛屽睘浜屾鍙栨暣锛屼笉閲囩敤銆傚悓鐞� 2026 瀵� 2024 涓� `-1.7710%` 鈫� `-1.8%`銆傚揩鎶ョ櫨鍒嗘瘮缁熶竴 1 浣嶅皬鏁板彛寰勪笉鍙樸��
+  - **2026-10-04 淇**锛氭寜 Excel 鍗曞厓鏍兼樉绀虹簿搴﹀彛寰勶紝`-4.65%` 鍐嶅彇涓�浣嶅簲涓� `-4.7%`锛涗互鏂囦欢椤堕儴鏈�鏂版涓哄噯銆�
 - **銆屾殏缂烘皯鑸�嶆彁绀�**锛氱敤鎴风‘璁ょ敱浜哄伐鍦ㄧ敓鎴愮殑蹇姤绋夸笂淇敼锛岀郴缁熶笉鐢熸垚璇ユ彁绀鸿锛�**涓嶅仛浠g爜鏀瑰姩**銆�
 - **楠岃瘉**锛歚HolidayBulletinServiceTest` 3 涓敤渚嬮�氳繃锛堟柊澧� 2 涓鐩栨皯鑸疮璁″姣旀湡闂翠笌鏃犳暟鎹繑鍥� null锛夛紱绔埌绔敤搴撳唴鐪熷疄 2026-10-02 鏁版嵁锛堥搧璺�/閬撹矾/姘磋矾/杞︽祦榻愬叏銆佺己姘戣埅锛夐噸鏂扮敓鎴愬揩鎶ワ紝姘戣埅绱鍚屾瘮 `-10.0%/-0.9%`銆佹皯鑸疮璁� `6.75`銆佹�昏绱 `3082.24`銆佹�昏绱鍚屾瘮 `-4.6%/-1.8%`锛屼笌鎵嬪伐绋垮彛寰勪竴鑷淬��
 - **Git**锛氭湰娆℃敼鍔ㄥ緟鐢ㄦ埛銆岄獙鏀堕�氳繃銆嶅悗鍐嶆彁浜�/鎺ㄩ�併��
@@ -33,7 +55,7 @@
 ### 10-03 娣卞锛�3锛夛細蹇姤绱姝f枃鍗曚綅鐢便�屼嚎浜烘銆嶆敼銆屼竾浜烘銆嶏紙寰呴獙鏀讹級
 
 - **鍙e緞锛堢敤鎴�/棰嗗纭锛�**锛氬亣鏈熺疮璁℃暟鍊肩洰鍓嶅亸灏忥紙0.31 浜匡級锛屾鏂囧彲璇绘�у樊锛屾敼涓轰笌褰撴棩涓�鑷寸殑**涓囦汉娆�**锛涗緥濡� `鍋囨湡浠ユ潵锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺绱3088.63涓囦汉娆锛屼笉鍐嶈緭鍑恒��0.31浜夸汉娆°�嶃�傝〃鏍间笌鍏朵綑鍙e緞涓嶅彉銆�
-- **瀹炵幇**锛歚HolidayBulletinService` 鎶藉嚭 `cumulativeParagraph(...)`锛岀洿鎺ユ寜涓囦汉娆★紙2 浣嶅皬鏁帮級杈撳嚭绱鍊硷紱鍚屾瘮浠嶄负銆屽闀�/涓嬮檷 + 涓�浣嶅皬鏁般�嶏紝**淇濇寔涓�娆″彇鏁翠笉鍙�**锛堟�昏绱鍚屾瘮 2025 绮剧‘ -4.4486% 鈫� `-4.4%`锛涙墜宸ョ鐨� -4.5% 灞炰簩娆″彇鏁达紝涓嶉噰鐢級銆�
+- **瀹炵幇**锛歚HolidayBulletinService` 鎶藉嚭 `cumulativeParagraph(...)`锛岀洿鎺ユ寜涓囦汉娆★紙2 浣嶅皬鏁帮級杈撳嚭绱鍊硷紱鍚屾瘮浠嶄负銆屽闀�/涓嬮檷 + 涓�浣嶅皬鏁般�嶏紝**淇濇寔涓�娆″彇鏁翠笉鍙�**锛堣澶� 2026-10-04 宸蹭慨姝o細婧愬崟鍏冩牸涓� `0.00%`锛孍xcel 鏄剧ず -4.45%锛屽啀鍙栦竴浣嶄负 -4.5%锛涗互鏂囦欢椤堕儴鏈�鏂版涓哄噯锛夈��
 - **楠岃瘉**锛歚HolidayBulletinServiceTest` 4 涓敤渚嬮�氳繃锛堟柊澧� `cumulativeParagraphUsesWanUnitAndSingleRounding` 鍥哄寲銆屼竾浜烘 + 涓�娆″彇鏁淬�嶏級锛涢噸鍚悗绔悗閲嶆柊鐢熸垚 2026-10-02 蹇姤锛屾鏂囦负 `绱3088.63涓囦汉娆★紝姣�2025骞村悓鏈熶笅闄�4.4%锛屾瘮2024骞村悓鏈熶笅闄�1.6%`锛屼笌鎵嬪伐绋夸粎宸棦瀹氱殑鍙栨暣鍙e緞锛�2026-10-01 鍥炲綊姝e父锛坄绱1643.17涓囦汉娆锛夈��
 - **鐘舵��**锛氬緟鐢ㄦ埛銆岄獙鏀堕�氳繃銆嶅悗鎻愪氦/鎺ㄩ�併��
 
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java
index f99c55d..312d18f 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java
@@ -42,6 +42,8 @@
 
     private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
     private static final BigDecimal EPSILON = new BigDecimal("0.0000001");
+    private static final int EXCEL_ONE_DECIMAL_PERCENT = 1;
+    private static final int EXCEL_TWO_DECIMAL_PERCENT = 2;
 
     @Value("${holiday.bulletin-template:docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/婀栧寳鐪佸浗搴嗗亣鏈熺患鍚堣繍杈撳娴佸揩鎶ユā鏉�.docx}")
     private String bulletinTemplatePath;
@@ -79,12 +81,12 @@
 
         String currentTotal = formatNumber(current.getTotalPersonFlow());
         String ringRatioClause = previousDay == null ? "" : "锛岀幆姣�" + ratioPhrase(
-                current.getTotalPersonFlow(), previousDay.getTotalPersonFlow());
+                current.getTotalPersonFlow(), previousDay.getTotalPersonFlow(), EXCEL_TWO_DECIMAL_PERCENT);
         String paragraph1 = String.format("%d鏈�%d鏃ワ紙鍋囨湡绗�%d澶╋級锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺%s涓囦汉娆�%s锛屾瘮2025骞村悓鏈�%s锛屾瘮2024骞村悓鏈�%s銆�",
                 date.getMonthValue(), date.getDayOfMonth(), current.getDayIndex(), currentTotal,
                 ringRatioClause,
-                ratioPhrase(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow()),
-                ratioPhrase(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow()));
+                ratioPhrase(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow(), EXCEL_ONE_DECIMAL_PERCENT),
+                ratioPhrase(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(), EXCEL_ONE_DECIMAL_PERCENT));
         String paragraph2 = cumulativeParagraph(currentCumulative, previousYearCumulative, twoYearsAgoCumulative);
 
         File template = resolveTemplateFile();
@@ -131,34 +133,46 @@
                 current, twoYearsAgo, DailyMetrics::getCivilAviation);
         writeTableRow(table, 2, new String[]{
                 number(current.getNonBusinessPersonFlow()),
-                ratio(current.getNonBusinessPersonFlow(), previousDay == null ? null : previousDay.getNonBusinessPersonFlow()),
-                ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow()),
-                ratio(current.getNonBusinessPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getNonBusinessPersonFlow()),
+                ratio(current.getNonBusinessPersonFlow(), previousDay == null ? null : previousDay.getNonBusinessPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT),
+                ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow(),
+                        EXCEL_ONE_DECIMAL_PERCENT),
+                ratio(current.getNonBusinessPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getNonBusinessPersonFlow(),
+                        EXCEL_ONE_DECIMAL_PERCENT),
                 number(currentCumulative.getNonBusinessPersonFlow()),
                 ratio(currentCumulative.getNonBusinessPersonFlow(),
-                        previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow()),
+                        previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT),
                 ratio(currentCumulative.getNonBusinessPersonFlow(),
-                        twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getNonBusinessPersonFlow())
+                        twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getNonBusinessPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT)
         });
         writeTableRow(table, 3, new String[]{
                 number(current.getRoad()),
-                ratio(current.getRoad(), previousDay == null ? null : previousDay.getRoad()),
-                ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad()),
-                ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.getRoad()),
+                ratio(current.getRoad(), previousDay == null ? null : previousDay.getRoad(), EXCEL_ONE_DECIMAL_PERCENT),
+                ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad(), EXCEL_ONE_DECIMAL_PERCENT),
+                ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.getRoad(), EXCEL_ONE_DECIMAL_PERCENT),
                 number(currentCumulative.getRoad()),
-                ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad()),
-                ratio(currentCumulative.getRoad(), twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoad())
+                ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad(),
+                        EXCEL_ONE_DECIMAL_PERCENT),
+                ratio(currentCumulative.getRoad(), twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoad(),
+                        EXCEL_ONE_DECIMAL_PERCENT)
         });
         writeTableRow(table, 4, new String[]{
                 number(current.getRoadPersonFlow()),
-                ratio(current.getRoadPersonFlow(), previousDay == null ? null : previousDay.getRoadPersonFlow()),
-                ratio(current.getRoadPersonFlow(), previousYear == null ? null : previousYear.getRoadPersonFlow()),
-                ratio(current.getRoadPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getRoadPersonFlow()),
+                ratio(current.getRoadPersonFlow(), previousDay == null ? null : previousDay.getRoadPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT),
+                ratio(current.getRoadPersonFlow(), previousYear == null ? null : previousYear.getRoadPersonFlow(),
+                        EXCEL_ONE_DECIMAL_PERCENT),
+                ratio(current.getRoadPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getRoadPersonFlow(),
+                        EXCEL_ONE_DECIMAL_PERCENT),
                 number(currentCumulative.getRoadPersonFlow()),
                 ratio(currentCumulative.getRoadPersonFlow(),
-                        previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow()),
+                        previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT),
                 ratio(currentCumulative.getRoadPersonFlow(),
-                        twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoadPersonFlow())
+                        twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoadPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT)
         });
         writeTableRow(table, 5, transportRow(current.getRail(),
                 firstDayRailBase == null ? null : firstDayRailBase.getRail(),
@@ -166,41 +180,54 @@
                 twoYearsAgo == null ? null : twoYearsAgo.getRail(),
                 currentCumulative.getRail(),
                 previousYearCumulative == null ? null : previousYearCumulative.getRail(),
-                twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRail()));
+                twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRail(),
+                EXCEL_ONE_DECIMAL_PERCENT));
         writeTableRow(table, 6, transportRow(current.getCivilAviation(),
                 firstDayCivilAviationBase == null ? null : firstDayCivilAviationBase.getCivilAviation(),
                 previousYear == null ? null : previousYear.getCivilAviation(),
                 twoYearsAgo == null ? null : twoYearsAgo.getCivilAviation(),
                 currentCumulative.getCivilAviation(),
                 previousYearCivilCumulative == null ? null : previousYearCivilCumulative.getCivilAviation(),
-                twoYearsAgoCivilCumulative == null ? null : twoYearsAgoCivilCumulative.getCivilAviation()));
+                twoYearsAgoCivilCumulative == null ? null : twoYearsAgoCivilCumulative.getCivilAviation(),
+                EXCEL_ONE_DECIMAL_PERCENT));
         writeTableRow(table, 7, transportRow(current.getWater(),
                 previousDay == null ? null : previousDay.getWater(),
                 previousYear == null ? null : previousYear.getWater(),
                 twoYearsAgo == null ? null : twoYearsAgo.getWater(),
                 currentCumulative.getWater(),
                 previousYearCumulative == null ? null : previousYearCumulative.getWater(),
-                twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getWater()));
-        writeTableRow(table, 8, transportRow(current.getTotalPersonFlow(),
-                previousDay == null ? null : previousDay.getTotalPersonFlow(),
-                previousYear == null ? null : previousYear.getTotalPersonFlow(),
-                twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(),
-                currentCumulative.getTotalPersonFlow(),
-                previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(),
-                twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow()));
+                twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getWater(),
+                EXCEL_TWO_DECIMAL_PERCENT));
+        writeTableRow(table, 8, new String[]{
+                number(current.getTotalPersonFlow()),
+                ratio(current.getTotalPersonFlow(), previousDay == null ? null : previousDay.getTotalPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT),
+                ratio(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow(),
+                        EXCEL_ONE_DECIMAL_PERCENT),
+                ratio(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(),
+                        EXCEL_ONE_DECIMAL_PERCENT),
+                number(currentCumulative.getTotalPersonFlow()),
+                ratio(currentCumulative.getTotalPersonFlow(),
+                        previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT),
+                ratio(currentCumulative.getTotalPersonFlow(),
+                        twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT)
+        });
     }
 
     private String[] transportRow(BigDecimal current, BigDecimal previousDay, BigDecimal previousYear,
                                   BigDecimal twoYearsAgo, BigDecimal currentCumulative,
-                                  BigDecimal previousYearCumulative, BigDecimal twoYearsAgoCumulative) {
+                                  BigDecimal previousYearCumulative, BigDecimal twoYearsAgoCumulative,
+                                  int ringRatioExcelScale) {
         return new String[]{
                 number(current),
-                ratio(current, previousDay),
-                ratio(current, previousYear),
-                ratio(current, twoYearsAgo),
+                ratio(current, previousDay, ringRatioExcelScale),
+                ratio(current, previousYear, EXCEL_ONE_DECIMAL_PERCENT),
+                ratio(current, twoYearsAgo, EXCEL_ONE_DECIMAL_PERCENT),
                 number(currentCumulative),
-                ratio(currentCumulative, previousYearCumulative),
-                ratio(currentCumulative, twoYearsAgoCumulative)
+                ratio(currentCumulative, previousYearCumulative, EXCEL_ONE_DECIMAL_PERCENT),
+                ratio(currentCumulative, twoYearsAgoCumulative, EXCEL_ONE_DECIMAL_PERCENT)
         };
     }
 
@@ -322,11 +349,10 @@
         return builder.toString();
     }
 
-    private String ratioPhrase(BigDecimal current, BigDecimal base) {
-        if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return "鏃犲彲姣旀暟鎹�";
-        BigDecimal ratio = HolidayNumberUtils.divide(current, base);
-        BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100));
-        if (percent == null || percent.abs().compareTo(EPSILON) < 0) return "鎸佸钩";
+    private String ratioPhrase(BigDecimal current, BigDecimal base, int excelPercentScale) {
+        BigDecimal percent = bulletinPercent(current, base, excelPercentScale);
+        if (percent == null) return "鏃犲彲姣旀暟鎹�";
+        if (percent.signum() == 0) return "鎸佸钩";
         return (percent.signum() > 0 ? "澧為暱" : "涓嬮檷") + HolidayNumberUtils.format1(percent.abs()) + "%";
     }
 
@@ -351,18 +377,30 @@
         BigDecimal total = currentCumulative == null ? null : currentCumulative.getTotalPersonFlow();
         return String.format("鍋囨湡浠ユ潵锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺绱%s涓囦汉娆★紝姣�2025骞村悓鏈�%s锛屾瘮2024骞村悓鏈�%s銆�",
                 formatNumber(total),
-                ratioPhrase(total, previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow()),
-                ratioPhrase(total, twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow()));
+                ratioPhrase(total, previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT),
+                ratioPhrase(total, twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(),
+                        EXCEL_TWO_DECIMAL_PERCENT));
     }
 
-    private String ratio(BigDecimal current, BigDecimal base) {
-        if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return "鈥�";
-        BigDecimal ratio = HolidayNumberUtils.divide(current, base);
-        if (ratio == null) return "鈥�";
-        BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100));
+    private String ratio(BigDecimal current, BigDecimal base, int excelPercentScale) {
+        BigDecimal percent = bulletinPercent(current, base, excelPercentScale);
         if (percent == null) return "鈥�";
-        BigDecimal rounded = percent.setScale(1, RoundingMode.HALF_UP);
-        return rounded.signum() == 0 ? "0%" : rounded.toPlainString() + "%";
+        return percent.signum() == 0 ? "0%" : percent.toPlainString() + "%";
+    }
+
+    /**
+     * Mirrors the source Excel percentage cell format, then applies the bulletin's one-decimal rule.
+     * Excel stores full precision but displays 0.0% or 0.00%; reviewers compare against that display.
+     */
+    private BigDecimal bulletinPercent(BigDecimal current, BigDecimal base, int excelPercentScale) {
+        if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return null;
+        BigDecimal ratio = HolidayNumberUtils.divide(current, base);
+        if (ratio == null) return null;
+        BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100));
+        if (percent == null) return null;
+        BigDecimal displayed = percent.setScale(excelPercentScale, RoundingMode.HALF_UP);
+        return displayed.setScale(1, RoundingMode.HALF_UP);
     }
 
     private String number(BigDecimal value) {
diff --git a/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayBulletinServiceTest.java b/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayBulletinServiceTest.java
index bdef3e2..846023c 100644
--- a/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayBulletinServiceTest.java
+++ b/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayBulletinServiceTest.java
@@ -21,12 +21,16 @@
     void formatsTableRatiosAsSignedPercentages() throws Exception {
         HolidayBulletinService service = new HolidayBulletinService(null);
 
-        assertEquals("-4.7%", ratio(service, "95.3", "100"));
-        assertEquals("1.0%", ratio(service, "101", "100"));
-        assertEquals("0%", ratio(service, "100", "100"));
-        assertEquals("0%", ratio(service, "10004", "10000"));
-        assertEquals("鈥�", ratio(service, "100", null));
-        assertEquals("鈥�", ratio(service, "100", "0"));
+        assertEquals("-4.7%", ratio(service, "95.3", "100", 1));
+        assertEquals("-4.4%", ratio(service, "95.551398", "100", 1));
+        assertEquals("-4.5%", ratio(service, "95.551398", "100", 2));
+        assertEquals("-0.1%", ratio(service, "99.955", "100", 2));
+        assertEquals("0%", ratio(service, "99.955", "100", 1));
+        assertEquals("1.0%", ratio(service, "101", "100", 1));
+        assertEquals("0%", ratio(service, "100", "100", 1));
+        assertEquals("0%", ratio(service, "10004", "10000", 1));
+        assertEquals("鈥�", ratio(service, "100", null, 1));
+        assertEquals("鈥�", ratio(service, "100", "0", 1));
     }
 
     @Test
@@ -78,7 +82,7 @@
     }
 
     @Test
-    void cumulativeParagraphUsesWanUnitAndSingleRounding() throws Exception {
+    void cumulativeParagraphUsesExcelDisplayThenBulletinRounding() throws Exception {
         HolidayBulletinService service = new HolidayBulletinService(null);
 
         DailyMetrics current = new DailyMetrics();
@@ -94,7 +98,7 @@
         String text = (String) method.invoke(service, current, previousYear, twoYearsAgo);
 
         assertEquals(
-                "鍋囨湡浠ユ潵锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺绱3088.63涓囦汉娆★紝姣�2025骞村悓鏈熶笅闄�4.4%锛屾瘮2024骞村悓鏈熶笅闄�1.6%銆�",
+                "鍋囨湡浠ユ潵锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺绱3088.63涓囦汉娆★紝姣�2025骞村悓鏈熶笅闄�4.5%锛屾瘮2024骞村悓鏈熶笅闄�1.6%銆�",
                 text);
     }
 
@@ -107,11 +111,13 @@
         return method.invoke(service, current, comparisonYear, selector);
     }
 
-    private String ratio(HolidayBulletinService service, String current, String base) throws Exception {
-        Method method = HolidayBulletinService.class.getDeclaredMethod("ratio", BigDecimal.class, BigDecimal.class);
+    private String ratio(HolidayBulletinService service, String current, String base,
+                         int excelPercentScale) throws Exception {
+        Method method = HolidayBulletinService.class.getDeclaredMethod(
+                "ratio", BigDecimal.class, BigDecimal.class, int.class);
         method.setAccessible(true);
         BigDecimal currentValue = current == null ? null : new BigDecimal(current);
         BigDecimal baseValue = base == null ? null : new BigDecimal(base);
-        return (String) method.invoke(service, currentValue, baseValue);
+        return (String) method.invoke(service, currentValue, baseValue, excelPercentScale);
     }
 }

--
Gitblit v1.9.1