Files
aomin a7605ef8b8 chore(repo): 导入 M-Z 业务底座 V1.9 基线并建立仓库规范
按周工交付的 SEC-Z-01 清理后源码建立 M-Z 主干,并按 Git 仓库惯例重排目录。

- aomin-gateway/  后端工程 87 文件,原样保留,路径与构建命令未变
- demo/           演示前端与设备模拟器 9 文件(原 02_前端与设备端),非正式三端
- docs/handover/  交付与安全清理材料 6 份,内容未改
- docs/contract/  J-00 规格、OpenAPI、内部五接口、状态机、Mock 样例
- docs/reference/ 指纹与校验清单,含 84 个源码文件的 sha256 清单

目录重排只动位置不动内容:源码树 84 文件哈希与交付一致,逐文件自校验通过;
12 条 Flyway 迁移与交付包内镜像逐字节一致。

未纳入:迁移镜像目录(与工程内一套重复,改留指纹)、target/、证据 raw、
应急恢复脚本、评审用虚构台账数据。

安全:SEC-Z-01 15 处明文已清零,导入前重新扫描无新增命中,本仓库无历史污染。
行尾按交付字节原样保存(.gitattributes 设 * -text),不做归一化以免整树摘要失效。
2026-11-13 17:32:37 +08:00

253 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>选择家的位置</title>
<!--
百度地图选点(在线底图 + 离线兜底)
─────────────────────────────────────────────────────────
· 底图用百度 JS API v2.0,ak 从下方 AK 常量读;
· **坐标系必须转**:百度用 BD-09,我们库里存 GCJ-02,两者差 500~600 米。
取点后 bd09ToGcj02() 转换再回传,否则存进去的家会落在隔壁街道;
· ak 失效 / 没网 / 白名单没放行时,3 秒内自动跳 map-offline.html,不让用户看白屏。
-->
<style>
:root{--brand:#146b7a;--ink:#1b2430;--sub:#6b7784;--line:#e4eaef}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;height:100%;overflow:hidden}
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
background:#eef2f4;color:var(--ink);font-size:14px;display:flex;flex-direction:column}
.top{flex:0 0 auto;background:var(--brand);color:#fff;padding:11px 12px;display:flex;align-items:center;gap:9px;z-index:10}
.top button{background:rgba(255,255,255,.16);border:0;color:#fff;border-radius:9px;padding:7px 11px;font-size:13px;cursor:pointer}
.top h1{font-size:15px;margin:0;flex:1;font-weight:600}
.stage{flex:1;min-height:200px;position:relative}
#map{width:100%;height:100%}
/* 中心准星:拖地图选点,比"点哪儿算哪儿"在手机上准得多 */
#pin{position:absolute;left:50%;top:50%;transform:translate(-50%,-100%);pointer-events:none;z-index:5}
#pin svg{filter:drop-shadow(0 2px 4px rgba(0,0,0,.35))}
#pinTip{position:absolute;left:50%;top:50%;transform:translate(-50%,6px);font-size:10.5px;
color:#fff;background:rgba(20,40,55,.62);padding:2px 8px;border-radius:10px;pointer-events:none;z-index:5}
#loading{position:absolute;inset:0;background:#f2f6f8;display:flex;align-items:center;justify-content:center;
flex-direction:column;gap:10px;z-index:8;font-size:13px;color:var(--sub)}
#loading .sp{width:26px;height:26px;border:3px solid #d6e2e6;border-top-color:var(--brand);border-radius:50%;
animation:spin .9s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.bottom{flex:0 0 auto;background:#fff;border-top:1px solid var(--line);padding:10px 12px 12px;z-index:10}
.addr{font-size:13px;color:var(--ink);margin-bottom:5px;min-height:18px;line-height:1.5;font-weight:600}
.sub{font-size:11.5px;color:var(--sub);margin-bottom:9px;line-height:1.5}
.coord{font-family:Consolas,Menlo,monospace;font-size:11.5px;color:var(--brand);margin-bottom:9px}
.row{display:flex;gap:7px;align-items:center;margin-bottom:8px}
.row input{flex:1;min-width:0;border:1px solid var(--line);border-radius:8px;padding:8px 9px;font-size:12.5px;outline:none}
.btn{border:0;border-radius:9px;padding:11px 14px;font-size:14px;font-weight:600;cursor:pointer}
.btn-main{background:var(--brand);color:#fff;width:100%}
.btn-main:disabled{background:#c3d3d7}
.btn-line{background:#fff;border:1px solid var(--line);color:var(--ink);white-space:nowrap}
.BMap_cpyCtrl,.anchorBL{display:none!important}
</style>
</head>
<body>
<div class="top">
<button onclick="cancel()">← 返回</button>
<h1>选择家的位置</h1>
<button onclick="locate()">定位</button>
</div>
<div class="stage">
<div id="map"></div>
<div id="pin"><svg width="30" height="40" viewBox="0 0 30 40">
<path d="M15 0C6.7 0 0 6.7 0 15c0 11 15 25 15 25s15-14 15-25C30 6.7 23.3 0 15 0z" fill="#146b7a"/>
<circle cx="15" cy="15" r="5.5" fill="#fff"/></svg></div>
<div id="pinTip">拖动地图,把准星对准你家</div>
<div id="loading"><div class="sp"></div><span id="ldTxt">正在加载地图…</span></div>
</div>
<div class="bottom">
<div class="addr" id="addr">拖动地图选择位置</div>
<div class="sub" id="sub">地址会自动识别,选不准可以手动改</div>
<div class="coord" id="coord">坐标:—(GCJ-02)</div>
<div class="row">
<input id="mlng" placeholder="经度 113.39" inputmode="decimal">
<input id="mlat" placeholder="纬度 22.52" inputmode="decimal">
<button class="btn btn-line" onclick="applyManual()">填入</button>
</div>
<button class="btn btn-main" id="ok" onclick="confirmPick()">确定这个位置</button>
</div>
<script>
/* 百度 ak —— 不写死在源码里:部署时把 __BAIDU_AK__ 替换成自己的 AK,
或用 window.BAIDU_AK 注入;没有 AK 时本页会自动降级到离线地图。 */
var AK = window.BAIDU_AK || '__BAIDU_AK__';
/* ================= 坐标转换(必须) =================
百度 BD-09 / 国测局 GCJ-02 相差 500~600 米。
库里存 GCJ-02,所以「取点 → 转 GCJ」,「显示已知点 → 转 BD」。 */
function bd09ToGcj02(bdLng, bdLat) {
var x = bdLng - 0.0065, y = bdLat - 0.006;
var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * Math.PI * 3000 / 180);
var t = Math.atan2(y, x) - 0.000003 * Math.cos(x * Math.PI * 3000 / 180);
return { lng: z * Math.cos(t), lat: z * Math.sin(t) };
}
function gcj02ToBd09(lng, lat) {
var z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * Math.PI * 3000 / 180);
var t = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * Math.PI * 3000 / 180);
return { lng: z * Math.cos(t) + 0.0065, lat: z * Math.sin(t) + 0.006 };
}
var map = null, marker = null, geocoder = null;
var cur = null; // 当前选的 GCJ-02 坐标
var geoTimer = null;
var HOME = { lng: 113.3926, lat: 22.5176 }; // 中山市政府(GCJ-02)
function fallback(reason) {
var t = document.getElementById('ldTxt');
if (t) t.textContent = reason + ',切换到离线地图…';
setTimeout(function () { location.replace('map-offline.html'); }, 700);
}
function initMap() {
if (map) return; // callback 和轮询都可能触发,只认第一次
try {
map = new BMap.Map('map', { enableMapClick: false });
var c = gcj02ToBd09(HOME.lng, HOME.lat);
map.centerAndZoom(new BMap.Point(c.lng, c.lat), 16);
map.enableScrollWheelZoom(true);
map.enableDragging();
geocoder = new BMap.Geocoder();
document.getElementById('loading').style.display = 'none';
map.addEventListener('moveend', onMoveEnd);
onMoveEnd();
} catch (e) {
fallback('地图初始化失败');
}
}
/* 地图停稳后取中心点,转成 GCJ-02 并反查地址 */
function onMoveEnd() {
if (!map) return;
var c = map.getCenter();
var g = bd09ToGcj02(c.lng, c.lat);
cur = { lng: +g.lng.toFixed(6), lat: +g.lat.toFixed(6) };
document.getElementById('coord').textContent =
'坐标:' + cur.lng + ', ' + cur.lat + '(GCJ-02)';
document.getElementById('mlng').value = cur.lng;
document.getElementById('mlat').value = cur.lat;
// 逆地理编码要联网,慢一点,节流 400ms,别拖一下发十个请求
clearTimeout(geoTimer);
geoTimer = setTimeout(function () {
if (!geocoder) return;
geocoder.getLocation(new BMap.Point(c.lng, c.lat), function (r) {
if (!r) { return; }
var a = r.address || {};
var parts = [a.province, a.city, a.district, a.street, a.streetNumber].filter(Boolean);
var poi = (r.surroundingPois && r.surroundingPois[0]) ? r.surroundingPois[0].title : '';
// 街道名常常拼不出完整地址(百度在小区内部只回商圈),所以依次退:街道串 → 最近POI → 商圈
var main = parts.join('') || poi || a.business || '已选位置';
document.getElementById('addr').textContent = main;
document.getElementById('sub').textContent = poi && main !== poi
? '附近:' + poi
: '地址已识别,可手动补充门牌';
});
}, 400);
}
function applyManual() {
var lng = parseFloat(document.getElementById('mlng').value);
var lat = parseFloat(document.getElementById('mlat').value);
if (!isFinite(lng) || !isFinite(lat)) { alert('请填入有效的经纬度数字'); return; }
if (lng < 112.5 || lng > 114.5 || lat < 21.5 || lat > 23.5) {
alert('这个坐标不在中山附近,请填 113.x / 22.x'); return;
}
cur = { lng: +lng.toFixed(6), lat: +lat.toFixed(6) };
var b = gcj02ToBd09(lng, lat);
if (map) { map.centerAndZoom(new BMap.Point(b.lng, b.lat), 17); }
}
function locate() {
if (!navigator.geolocation) { alert('这个浏览器不支持定位'); return; }
navigator.geolocation.getCurrentPosition(function (p) {
// 浏览器给的是 WGS-84,百度要 BD-09:先粗转 GCJ 再转 BD(市域内误差 <10m)
var g = wgs84ToGcj02(p.coords.longitude, p.coords.latitude);
var b = gcj02ToBd09(g.lng, g.lat);
if (map) map.centerAndZoom(new BMap.Point(b.lng, b.lat), 17);
}, function () {
alert('定位失败(可能是没授权或信号不好),直接拖地图选吧');
}, { timeout: 8000 });
}
/* WGS-84 → GCJ-02(国内加密偏移的常用近似实现) */
function wgs84ToGcj02(lng, lat) {
var a = 6378245.0, ee = 0.00669342162296594323;
var dLat = tfLat(lng - 105, lat - 35), dLng = tfLng(lng - 105, lat - 35);
var radLat = lat / 180 * Math.PI, magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
var sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI);
dLng = (dLng * 180) / (a / sqrtMagic * Math.cos(radLat) * Math.PI);
return { lng: lng + dLng, lat: lat + dLat };
}
function tfLat(x, y) {
var r = -100 + 2 * x + 3 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
r += (20 * Math.sin(6 * x * Math.PI) + 20 * Math.sin(2 * x * Math.PI)) * 2 / 3;
r += (20 * Math.sin(y * Math.PI) + 40 * Math.sin(y / 3 * Math.PI)) * 2 / 3;
r += (160 * Math.sin(y / 12 * Math.PI) + 320 * Math.sin(y * Math.PI / 30)) * 2 / 3;
return r;
}
function tfLng(x, y) {
var r = 300 + x + 2 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
r += (20 * Math.sin(6 * x * Math.PI) + 20 * Math.sin(2 * x * Math.PI)) * 2 / 3;
r += (20 * Math.sin(x * Math.PI) + 40 * Math.sin(x / 3 * Math.PI)) * 2 / 3;
r += (150 * Math.sin(x / 12 * Math.PI) + 300 * Math.sin(x / 30 * Math.PI)) * 2 / 3;
return r;
}
function confirmPick() {
if (!cur) { alert('还没选位置'); return; }
var addr = document.getElementById('addr').textContent || '';
var d = {
type: 'heserv:pick',
lng: cur.lng, lat: cur.lat,
name: addr.length > 24 ? addr.slice(0, 24) : addr,
addr: addr,
coordSys: 'GCJ-02',
source: 'baidu'
};
if (window.parent && window.parent !== window) {
window.parent.postMessage(d, '*');
} else {
alert('已选:' + d.lng + ', ' + d.lat);
}
}
function cancel() {
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: 'heserv:pick-cancel' }, '*');
} else { history.back(); }
}
/* ================= 加载百度脚本 =================
注意:百度 v2.0 的 loader 内部用 document.write 注入主脚本。
如果用 createElement 动态插入,write 在页面加载完后调用会被浏览器忽略,
BMap 永远不出现 —— 表现就是"一直加载中,最后跳离线版"。
所以这里必须用**静态 script 标签**,并靠 &callback= 拿加载完成的通知。 */
function onBMapReady() {
window.__bmapOk = true;
setTimeout(initMap, 80);
}
(function () {
var timer = setInterval(function () {
if (window.BMap && window.__bmapOk !== false) {
clearInterval(timer);
if (!map) initMap();
return;
}
if (window.__bmapTries === undefined) window.__bmapTries = 0;
if (++window.__bmapTries > 22) { // 约 4.4 秒还没起来
clearInterval(timer);
if (!map) fallback('地图加载超时');
}
}, 200);
})();
</script>
<script src="https://api.map.baidu.com/api?v=2.0&ak=__BAIDU_AK__&callback=onBMapReady"></script>
</body>
</html>