20 KiB
祈运小程序后端
Spring Boot 后端,提供微信小程序登录、手机号验证码绑定、用户资料、协议和关注我们接口。
命理计算使用 cn.6tail:lunar 根据公历生日和时辰生成四柱八字,再计算五行分布、日主、喜用和忌用。
元宝页提供今日卡牌和每日生活小贴士接口。
本地启动
export WECHAT_APP_ID=wxa76a2ef9e8ba612e
export WECHAT_APP_SECRET='set-via-environment'
export DB_URL='jdbc:mysql://101.43.139.56:3306/hera?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true'
export DB_USERNAME=hera_user
export DB_PASSWORD='请通过部署环境配置'
mvn spring-boot:run
默认地址:http://localhost:8080
本机敏感配置可放在仓库根目录的 .hera-local.yml,使用与 application.yml 相同的 YAML
层级。默认启用 local,从当前启动目录读取该文件;此文件已加入 Git 忽略清单。
环境与部署
application.yml:公共参数、上传限制与日志策略。application-local.yml:本地环境,MySQL/Redis 默认使用公网101.43.139.56,允许创建缺失表。application-prod.yml:线上环境,MySQL/Redis 默认使用内网10.0.8.9,不自动建表或改表。.hera-local.yml:本机私有配置,仅local环境加载。.hera-prod.yml:线上私有配置,仅prod环境加载,不会误读本地配置。
两套环境不能同时启用。地址和端口可通过 DB_URL、REDIS_HOST、REDIS_PORT、
SERVER_PORT 覆盖,其他参数仍使用现有环境变量。线上必须配置至少 32 字节、非开发默认值的
hera.jwt-secret(或 HERA_JWT_SECRET),不满足时拒绝启动。短信仍为 mock,正式上线前需接入厂商。
环境隔离只隔离配置文件,不会自动创建两套数据库;需要数据隔离时应分别指定数据库和 Redis 逻辑库。
服务器使用 Java 8,将 JAR 与私有 .hera-prod.yml 放在同一启动目录,执行:
chmod 600 .hera-prod.yml
java -jar hera-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
私有文件字段结构参考 deploy/hera-prod.yml.example;可以直接填写真实值,也可以使用环境变量占位符。
本机已按现有密钥准备私有 .hera-prod.yml,未复制本地的公网连接地址覆盖线上内网地址。
这两个私有文件均不进入 Git 或 JAR,不应放在 Nginx 可下载目录中。
需要将私有文件置于其他目录时,可设置 HERA_PROD_CONFIG=optional:file:/etc/hera/prod.yml
或对应的 HERA_LOCAL_CONFIG,默认仍从启动目录查找。
环境变量占位符只在未被私有文件的同名直接值覆盖时生效;若要通过环境变量管理某字段,
请删除私有文件中的直接值或将该值改成对应占位符。
日志
控制台与文件同时输出,时间使用北京时间。默认路径为启动目录的 logs/,
可设置 LOG_PATH=/var/log/hera,启动用户必须具有目录写权限:
hera.log:启动信息、请求摘要及异常等全部应用日志。hera-error.log:仅 ERROR 级别日志,错误同时保留在hera.log。archive/:按日期和大小滚动后的 gzip 压缩日志。
默认每文件 20MB、保留 30 天,每个文件序列独立限制总量为 2GB(两个序列合计最高约 4GB,
不含当前活动文件);支持 LOG_MAX_FILE_SIZE、LOG_MAX_HISTORY、LOG_TOTAL_SIZE_CAP 覆盖。
应用日志不代替 Nginx 访问日志、指标和告警,磁盘仍需运维监控。
每个请求返回 X-Request-Id,对应日志中的 requestId。请求摘要记录方法、路由模板、
状态码和耗时,不记录查询串、请求或响应正文、手机号、验证码、Token、Cookie、文件凭证等。
未知路由只记 <unmapped>,不打印可能包含敏感信息的原始路径。异常日志保留类型、原因链类型和
堆栈位置,但不打印异常消息,以免泄露 SQL 参数或包含微信密钥的 URL。
默认不启用 SQL 参数与 HTTP wire DEBUG 日志,不建议在线上自行打开此类日志。
tail -f logs/hera.log
tail -f logs/hera-error.log
Redis
使用 Spring Data Redis 和 Lettuce,依赖版本由 Spring Boot 2.7.18 管理。
本地默认地址为 101.43.139.56:6379,线上为 10.0.8.9:6379,逻辑库为 0,连接与命令超时均为 3 秒。
支持 REDIS_HOST、REDIS_PORT、REDIS_USERNAME、REDIS_PASSWORD、REDIS_DATABASE、
REDIS_SSL 环境变量;本机密码配置在被 Git 忽略的 .hera-local.yml 中。
业务代码可注入 StringRedisTemplate。Key、Value 和 Hash 均采用字符串序列化;
结构化数据使用现有 ObjectMapper 显式转换为 JSON。建议 Key 使用 hera:{业务}: 前缀,
缓存和临时数据写入时必须指定合理的 TTL。当前接入连接能力,登录会话、mock 短信验证码
仍由现有 MySQL 链路保存,未启用用户资料自动缓存。
mvn test 包含不访问真实 Redis 的配置测试。
mvn test -Dtest=RedisIntegrationTest -Dhera.redis.integration=true 显式验证真实连接、
认证、PING、字符串读写与 TTL,使用 hera:test:redis:{随机值} 测试键并在结束后删除,
不清空 Redis。该测试独立于 MySQL,不执行用户表迁移。
腾讯云 COS 文件存储
存储桶为 hera-1319913268,地域为 ap-beijing,桶权限保持私有读。
运行环境必须配置 TENCENT_COS_SECRET_ID、TENCENT_COS_SECRET_KEY。
推荐仅授权该桶 hera/avatar/*、hera/files/* 路径的 cos:PutObject、cos:GetObject 权限。
密钥只通过环境变量或被忽略的私有配置提供,不进入受版本管理的配置或 Git。
未配置 COS 密钥时允许服务启动,文件操作会返回配置错误。
POST /api/user/avatar:上传 JPG、PNG、WebP 头像,最大 5MB。POST /api/files:通过 multipart 字段file上传普通文件,最大 5MB。GET /api/files/url?reference=cos%3A%2F%2Fhera%2Ffiles%2F...:为本人文件续签。
所有上述接口需要 Bearer JWT。对象分别存储在 hera/avatar/{userId}/、
hera/files/{userId}/,数据库保存 cos:// 对象标识;不要保存临时签名 URL。
资料查询返回的头像 URL 和文件上传返回的 URL 有效期为 900 秒;过期后重新获取。
历史 /uploads/avatars/ 路径继续按原有本地静态资源配置访问,旧文件没有自动迁移。
访问入口默认使用 http://101.43.139.56/hera/file/access?token=...,Nginx 将外部
/hera/file/access 转发至后端 /file/access,保留查询参数;参考
deploy/nginx-file.conf.example。不要将携带应用文件凭证的入口直接代理至 COS。后端校验文件专用短期凭证后,
302 跳转至 COS 官方 HTTPS 签名 URL。入口不要求登录头,必须携带有效文件凭证。
文件链接属于临时访问凭据,不应写入日志或分享给无权访问的人。
部署时通过 HERA_JWT_SECRET 配置随机签名密钥,并通过 TENCENT_COS_PUBLIC_BASE_URL
配置生产 HTTPS 域名下的 /file/ 地址。小程序需配置入口和跳转目标的 HTTPS 合法域名。
当前 HTTP IP 地址供联调使用,不满足正式小程序的 HTTPS 域名要求。
如果部署环境或私有配置仍将 TENCENT_COS_PUBLIC_BASE_URL/hera.cos.public-base-url
设为旧的 /file/ 地址,需要一并改为 http://101.43.139.56/hera/file/,否则会覆盖默认值。
更新配置并重启服务后,重新获取资料或天宫列表生成新链接;已有缓存链接不会自动改变。
COS 验证
mvn test 执行本地签名、目录归属和模拟上传测试,不访问真实 COS。
mvn test -Dhera.cos.integration=true 额外启用真实 COS 联调:从运行配置读取密钥,
在 hera/avatar/9223372036854775807/ 和 hera/files/9223372036854775807/
创建本次随机命名的测试对象,校验匿名访问为 403、签名下载内容一致,以及后端文件入口的跳转。
测试结束后删除本次创建的对象,因此联调账号还需要这两个测试目录的 cos:DeleteObject 权限。
此测试不会操作数据库,也不会验证远端 Nginx 是否已部署。
开发说明
- 当前已默认关闭微信 mock,并接入微信
jscode2session。 - 数据持久层使用 MyBatis Mapper XML,本地启动会执行
schema.sql创建缺失的数据表,线上默认不执行。 - 本地数据库连接默认指向
101.43.139.56:3306/hera,线上默认内网10.0.8.9:3306/hera,可通过环境参数覆盖。 WECHAT_APP_SECRET请优先使用服务器环境变量配置,不建议提交到公开仓库。/api/auth/sms开发阶段返回mockCode,接入短信厂商后移除返回值。- 正式上线前需要替换
hera.jwt-secret、短信服务实现和协议正文。
数据表 CRUD
小程序用户业务已统一到 user_account 与 user_profile:微信身份、昵称和手机号
保存在账号表;性别、生日、出生时辰、头像、现居住地区与资料修改额度保存在资料表。
User 仅作为联合查询模型;业务接口不再读写 hera_user。
当前系统尚未上线,以当前数据库与 schema.sql 为准,不执行历史用户迁移、
启动时补列或修改旧表结构,也不读取或维护历史迁移版本表。
schema.sql 仅通过 CREATE TABLE IF NOT EXISTS 初始化缺失的当前业务表;
已有表结构若发生变化,应先更新代码与建表定义,不自动兼容历史结构。
纯微信账号的 password_hash 可空;资料修改周期为首次修改起六个月,最多五次。
历史迁移源码与对应测试已移除,但本次清理不会删除数据库中已有的表或记录。
POST /api/auth/wx-login 对已绑定手机号的用户额外返回 token 和 user;未绑定时,
继续通过 sessionKey 和 mock 短信验证码调用 /api/auth/bind-phone 完成注册或绑定。
绑定成功会核销会话与验证码。禁用账号不能登录或使用个人资料接口。
个人资料接口的字段名保持不变;资料尚未填写时,生日、时辰及五行报告可能为空,不填充虚构资料。
原 /api/data/** 通用 CRUD 控制器已撤下,避免普通登录用户改写设备归属、资产、
账号状态或他人资料。Mapper 与内部 CRUD Service 保留,不作为小程序 HTTP 接口。
账号注册和资料修改继续使用 /api/auth/** 与 /api/user/profile,尚未提供管理员接口。
NFC 与虚拟物
全部接口需要 Authorization: Bearer <token>,用户身份取 JWT,不接收操作者用户 ID。
列表接受 offset=0&limit=20,单页最大 100。芯片和虚拟物字典由可信后台或数据库预先入库,
不允许小程序自行注册硬件、指定设备奖励或修改流水。
代码以现库结构为准:绑定流水通过 nfc_uid 关联设备,芯片不再使用 last_login_at。
nfc_chip.sn 为设备序列号,按现库定义使用 VARCHAR(64) NOT NULL,无默认值、无唯一索引。
硬件入库和更新时必须提供 SN;扫描、绑定及本人设备列表返回 sn,客户端不能通过绑定请求修改它。
设备归属和绑定历史仍以唯一 UID 关联,不将 SN 当作唯一标识或绑定凭证。
新库由 schema.sql 创建该字段;旧库若尚未新增,应使用真实序列号补录已有设备后再设置非空约束,
不自动生成序列号或用 UID 代填,本服务不在启动时自动修改已有表。
旧库如缺少芯片归属、解绑额度或绑定流水表,可先备份,再手工执行
src/main/resources/db/nfc-schema-update.sql;该脚本不由应用自动执行。新库由 schema.sql 建表。
若已有同名表但字段结构不同,部署前应核对,不会通过删表自动适配。
-
POST /api/nfc/scan:{"uid":"设备UID"},查看绑定状态,不返回他人身份或 NDEF。 -
POST /api/nfc/bind:同上,绑定设备、写绑定流水、激活预置虚拟物,整体事务提交。 -
POST /api/nfc/unbind:同上,解绑本人设备并写解绑时间。 -
GET /api/nfc/devices:本人当前设备。 -
GET /api/nfc/bindings:本人绑定与解绑历史。 -
GET /api/virtual-items:启用的虚拟物字典,按字典排序。 -
GET /api/tiangong/items:本人神仙持有状态、上香计数及展示图片。 -
GET /api/user/virtual-items:本人资产,含失效与送神状态。 -
GET /api/user/virtual-items/{id}:本人持有关系详情。 -
PUT /api/user/virtual-items/sort:{"ids":[持有关系ID,...]},按提交顺序设置排序值。 -
POST /api/user/virtual-items/offerings:{"userVirtualItemId":1,"offeringType":2,"prayerContent":"愿家人平安"}。 -
POST /api/user/virtual-items/lifecycle:{"userVirtualItemId":1,"actionType":1},1请神、2送神。 -
GET /api/user/virtual-items/lifecycle?holdingId=1:本人请神/送神流水,holdingId可省略。 -
GET /api/user/virtual-items/offerings?holdingId=1:本人供奉记录,holdingId可省略。 -
POST /api/user/virtual-items/dedications:{"userVirtualItemId":1,"dedicationTarget":"家人","dedicationContent":"愿平安顺遂"}。 -
GET /api/user/virtual-items/dedications:本人发起的回向记录。 -
/api/user/virtual-items/transfers保留为上述回向接口的路径别名,使用相同请求和响应,不再接收接收账号 ID。
GET /api/tiangong/items 还会为每位神仙返回 latestPrayer(prayerContent)和
latestDedication(dedicationTarget、可空的 dedicationContent),供祈愿、回向表单回显。
仅匹配当前登录用户与该神仙字典 ID 的 status=1 且存在对应明细的记录;
两类分别按 offered_at DESC, id DESC 取最新一条,没有可用记录时对应字段为 null。
虚拟物字典与天宫列表均返回 iconUrl(列表图标)和 iconDetailUrl(神仙详情页大图),
两种图片独立配置,不互相覆盖。virtual_item.icon_detail_url 按现库使用
VARCHAR(255) NOT NULL、无默认值,后台入库和更新字典时必须提供,已有表不由启动流程自动改列。
私有桶图片与头像一致:数据库保存 cos://hera/virtual-items/{itemId}/... 持久标识,
上述两个业务接口实时返回有效期 900 秒的文件访问入口,不向数据库写入临时签名 URL。
已有普通 HTTP/HTTPS 图片地址保持原样。文件网关允许签名访问该专用目录,但不将它视为用户个人文件。
COS 身份需具有该目录下的 PutObject、GetObject 权限;不改变存储桶私有读权限。
VirtualItemImageImportTest 是显式授权才会执行的真实数据导入,默认跳过。
启用参数为 -Dhera.virtual-item.image-import=true,并且必须同时指定
hera.import.item-id、hera.import.item-name、hera.import.icon-path、hera.import.detail-path。
它校验目标名称、PNG/WebP 文件头与 5MB 大小限制,按 SHA-256 命名对象并验证签名下载内容,
先在 target/ 备份旧图片值,再通过旧值条件同时更新两个图片字段;不修改其他字典项。
-Dhera.import.inspect-only=true 只列出 ID 与名称,不上传或写库;新增缺失字典项必须显式设置
-Dhera.import.create-if-missing=true,不允许将已有同名神仙用其他 ID 重复创建。
批量导入可通过 -Dhera.import.manifest=/absolute/path/manifest.json 指定 JSON 数组,
每项包含 id、name、iconPath、detailPath;批次内 ID 和名称不可重复。
WebP 应在上传前用支持该格式的工具完整解码,核对尺寸、透明通道与压缩质量,
导入器不会将不支持 WebP 的 JDK ImageIO 当作解码器;原始文件不应被压缩结果覆盖。
不要在普通构建、CI 或无人审核的自动化中启用此参数。
一个芯片仅允许一个当前用户,一个微信用户可以绑定多个芯片;要求有效账号已绑定微信和手机号。 重复绑定本人设备只刷新更新时间,不增加绑定次数,也不重复发放资产。 每台芯片按北京时间自然年最多解绑两次,次数跟随芯片,不因换用户而重置,跨年重新计算。
同类虚拟物在一个用户下只有一条资产记录,保留首次获得的 nfc_uid 和获得时间。
绑定新设备可恢复该类失效资产。解绑最后一台提供该类虚拟物的设备后,资产状态置为 3(失效),
不删除供奉和回向历史;仍有同类设备时保留资产。失效资产不能互动或直接请神,必须重新绑定。
供奉类型与最新表设计一致:1 上香、2 祈愿、3 回向。原来的 1 请神、5 送神
必须改为调用独立的 /lifecycle 接口,客户端需随本次后端适配同步升级,不能沿用旧供奉编码。
请神/送神写 user_virtual_item_lifecycle,其中 action_type 为 1 请神、2 送神。
绑定首次发放或恢复资产也写请神流水;重复绑定本人设备或添加同类设备不重复写请神流水。
送神置状态 0,保有对应设备时可通过请神恢复为 1。重复请神、重复送神会拒绝。
供奉主记录只保留用户、虚拟物、类型、状态和时间,明细分别写入
user_virtual_item_offering_incense、user_virtual_item_offering_prayer、
user_virtual_item_offering_dedication,主记录与明细在同一事务提交。
上香可传 incenseCount(3、6、9,默认3)、incenseType(可省略,1至127)、remark(最多255字)。
每次请求由服务端记录 incenseTimes=1;客户端不能通过该字段扩大次数。
祈愿必须填写 prayerContent(非空,最多500字,兼容旧字段名 content)。
通过供奉入口回向必须填写 dedicationTarget(非空,最多255字),dedicationContent 可为空、最多500字。
同一请求只允许填写所选类型的明细,meritValue 仍为禁用参数,填写即拒绝。
响应中按类型返回嵌套 incense、prayer、dedication 明细,其余两个为 null。
holdingId 历史筛选先校验归属,再按当前用户和虚拟物查询,不依赖主表已删除的持有关系字段。
天宫的 incenseCount 是本人该虚拟物有效上香主记录所关联明细的 SUM(incense_times),
不按香支数或记录条数计算,无记录时返回0;祈愿、回向和无效主记录不参与累计。
回向只保存发起用户、虚拟物、对象文本和可选的回向文;不关联接收账号,不转移资产。
专用回向接口兼容 name/content 字段别名,返回统一的供奉主记录和 dedication 明细。
回向列表仅返回当前用户发起的记录,不通过对象文本推测“接收人”。
记录不开放通用修改和删除。UID 校验仅确认已入库设备,不具备防复制能力;
若产品需要防克隆,须增加硬件挑战响应或服务端签发的激活凭证,不能将普通 UID 当作秘密。
NfcWorkflowTest 在独立 H2 MySQL 模式测试库验证真实 Mapper XML、HTTP、事务与并发抢绑,
覆盖解绑上限、跨年、同类多设备、失效恢复、越权、供奉和只记账回向,不访问线上业务数据。
mvn test -Dtest=NfcMySqlIntegrationTest -Dhera.nfc.mysql.integration=true 显式检查现库列名,
通过只读、零行查询验证 XML 与现有供奉拆分表兼容,再在十张连接级临时表中验证读写与回滚。
新增请神/送神流水表在临时表中验证,不要求联调前向正式库建表。
临时表创建成功并全部覆盖后才允许测试写入;结束时回滚并关闭连接自动清理,
不修改正式表结构和业务记录。测试账号需有 CREATE TEMPORARY TABLES 权限。
供奉拆分后的部署
本次适配以已经存在的供奉主表及三张明细表为前提;部署新代码前,执行
src/main/resources/db/virtual-item-lifecycle-update.sql 创建独立的请神/送神流水表。
脚本可重复执行,不更新已有业务记录、不推测回填历史。新环境由 schema.sql 创建全套表。
旧的 named-dedication-update.sql 只适用于历史回向表,不能在当前已拆分的数据库上重放。
从更早的旧供奉结构升级时,需另行制定历史数据迁移,不能只重命名类型编码或通过删表重建处理。