OnecardDemo Change 单点功能与字段说明
| 项目 | 内容 |
|---|---|
| 文档版本 | 1.10 |
| 日期 | 2026-08-24 |
| 对应实现 | /Users/liang/AndroidStudioProjects/OnecardDemo |
| 适用范围 | 本地消费、服务器连接与查询、消费记录上传 |
| 一致性口径 | 本文必填性、字段名、类型与页面/Java 实现 1:1 对应 |
1. 状态说明
| 能力 | 实现状态 | 验证状态 |
|---|---|---|
| 主钱包本地消费 | 已实现 | Windows 1 分抓包已确认;Android 真卡扣款待验证 |
| 补助钱包本地消费 | 已实现 | 读钱包已确认;需要有补助余额的 CPU 卡验证扣款 |
| 补贴优先自动分账 | 已实现 | 计划与部分成功语义已单测;真卡两钱包场景待验证 |
| SOAP 请求生成、连接与查询 | 已实现 | YF_029E 真实 WebService 连接成功,用户总数返回 25965;单个用户已完成字段解析与真机验证 |
| 全部用户 SQLite 持久化 | 已实现 | 51 字段解析、唯一性、批事务和页面相关真机测试通过;现场全量同步待执行 |
| 全量同步分阶段耗时日志 | 已实现 | JVM 成功/失败阶段和敏感信息边界测试通过;真机日志文件与现场全量同步待验证 |
| 一键服务器稳定性测试 | 已实现 | YF_029E 三轮共 300 次全部成功,综合成功率 100%,平均 107.09 ms、P95 129 ms、最大 336 ms;最大值为第三轮首请求且成功 |
| 消费记录上传 | 已实现 | WSDL 请求结构单测通过;服务端接收和返回码待联调 |
“已实现”表示 Android 页面和 Java 调用链存在,不代表缺少现场条件的功能已经验收。
2. 本地消费单点功能
2.1 页面输入
| 页面字段 | 控件 ID | 必填 | Java 类型 | 校验与转换 |
|---|---|---|---|---|
| 账号 | input_consume_account |
是 | long |
分步读卡、一键读卡或自动读卡成功后自动填入卡内 CustomerID;用户仍可修改;执行前必须与当前卡内账号一致。不会填入 CardNO |
| 消费额 | input_consume_amount |
是 | long |
单位为分;必须大于 0,单笔不超过无符号 32 位范围且不得超过目标钱包余额 |
| 钱包策略 | spinner_consume_wallet |
是 | 枚举/策略 | 补贴优先(自动分账)、主钱包(0)、补助钱包(1) 三选一 |
| 消费时间 | input_consume_time_millis |
自动 | 页面显示 String,内部 long |
页面首次打开显示当前系统时间;点击“本地消费”时重新获取当前时间,不采用手工修改值;显示格式固定为 yyyy-MM-dd HH:mm:ss,内部毫秒值仍按设备默认时区转换为 yyyyMMddHHmmss 的 7 字节 BCD |
本地消费只能在读卡器已经连接、页面不忙且自动寻卡读卡开关关闭时执行。执行时内部会先寻卡和读卡,用户不需要额外点击“寻卡”或“读卡”。
主页面和服务器单点页面的所有可编辑输入框均为单行输入,软键盘回车动作统一显示“完成”。服务器消费记录上传中的 OPDT 是原始交易时间,仍由用户按 yyyy-MM-dd HH:mm:ss 填写,不会在上传时覆盖为当前时间。
2.2 钱包与分账规则
| 页面选择 | 代码值 | 卡文件 | 行为 |
|---|---|---|---|
| 主钱包 | 0 |
EC11 |
只扣主钱包 |
| 补助钱包 | 1 |
EC12 |
只扣补助钱包 |
| 补贴优先 | 最多两笔 | 先 EC12,后 EC11 |
先使用补助余额,不足部分再扣主钱包 |
补贴优先时,总余额不足会在写卡前拒绝;补助钱包余额为 0 时只产生主钱包交易;两边都参与时产生两笔独立卡上交易,每笔有各自的消费计数、PSAM 脱机交易序号和 TAC。
2.3 本地消费结果
| 结果字段 | Java 来源 | 含义 |
|---|---|---|
| 钱包 | ConsumptionResult.wallet |
主钱包或补助钱包 |
| 消费额 | amount |
单位分 |
| 扣款前/后余额 | balanceBefore / balanceAfter |
目标钱包余额,单位分 |
| 扣款前/后计数 | operationCountBefore / operationCountAfter |
目标钱包消费计数 |
| 消费时间 | timeMillis |
同时显示格式化时间和原始毫秒值 |
| PSAM 卡号 | psamIdHex / psamIdDecimal |
同时显示 6 字节十六进制和十进制 |
| PSAM 脱机交易序号 | psamTransactionNumber |
PSAM 80 70 返回值 |
| TAC | tac |
用户卡 80 54 返回的 4 字节交易验证码,页面按 8 位十六进制显示 |
| PSAM 确认 | psamConfirmed |
80 72 是否成功;失败不表示卡片未扣款 |
2.4 异常与禁止重试规则
| 错误 | 触发条件 | 处理 |
|---|---|---|
INVALID_INPUT |
缺字段、金额不大于 0、字段越界 | 写卡前拒绝 |
ACCOUNT_MISMATCH |
输入账号与卡内账号不一致 | 写卡前拒绝 |
CARD_STATUS_INVALID |
卡状态不是 0xF1 |
写卡前拒绝 |
INSUFFICIENT_BALANCE |
单钱包或总余额不足 | 写卡前拒绝 |
PSAM_ERROR |
PSAM 候选槽 2、3 均未返回 ATR/9000 |
依次探测两个槽;0101 转下一槽,其他异常仅在任何扣款 APDU 前关闭当前槽并重试一次;仍失败则显示各槽原始响应和长度 |
TRANSACTION_OUTCOME_UNKNOWN |
80 54 已发送但未获得可信响应 |
禁止直接重试;先重新读卡核对余额和计数 |
| PSAM 确认失败 | 卡片已返回 TAC,但 80 72 失败 |
返回已扣款结果且 psamConfirmed=false;禁止再次扣卡 |
| 自动分账部分成功 | 第一笔成功、第二笔失败 | 保留第一笔完整结果;不回滚、不自动重试 |
3. 服务器连接
服务器单点页面使用与 LogViewerActivity 一致的 ActionBar 左上角返回箭头;页面启用 setDisplayHomeAsUpEnabled(true),点击后由 onSupportNavigateUp() 调用 finish() 关闭 ServerOperationsActivity 并返回主页面,不发送服务器请求。内容区不再放置独立的普通“返回”按钮。
| 页面字段 | 控件 ID | 必填 | Java 类型 | 校验 |
|---|---|---|---|---|
| WebService 地址 | input_server_url |
是 | String |
只接受 http/https,路径必须以 .asmx 结尾 |
| 应用 ID | input_server_app_id |
是 | long |
不得留空,必须为十进制整数 |
SOAP 1.1 命名空间和 SOAPAction 前缀均为 http://NewCap.com/NewCapecWebService/。点击“连接服务器”会调用 GetCustomerCount(appId) 验证地址、应用 ID、HTTP 和 SOAP 响应;appId 的大小写必须与 WSDL 完全一致。成功后才启用查询和上传按钮。点击“断开服务器”只清除本页内存中的地址和应用 ID,不向服务器发送断开请求。
当前 Android 清单允许 HTTP 明文连接,是为了兼容厂商文档中的局域网 HTTP WebService;生产部署应优先使用 HTTPS 或按域名配置网络安全策略。
SOAP 响应解析兼容 Android 11 的 Harmony XML 实现:不再强制要求平台支持 Apache 专用的 disallow-doctype-decl feature。解析前会直接拒绝任何包含 DOCTYPE 的响应;对平台支持的安全 feature 继续关闭外部通用实体和外部参数实体,并通过 EntityResolver 二次拒绝外部实体。这样正常 SOAP XML 可以在 Android 上解析,同时维持 XXE 防护边界。
2026-08-24 对同一服务地址和 AppID 6 做了大小写对照:错误参数 <appid>6</appid> 稳定返回 1900009-在加载应用认证信息时,应用配置信息=null!,正确参数 <appId>6</appId> 返回用户总数 25965,与 Windows Demo 一致。根因是 Android 请求元素名大小写错误,不是缺少 Android 版 SysInit.dat 或 AppID 6 服务端配置。YF_029E 安装修复版后,连接服务器和单个用户查询均成功。
3.1 一键服务器稳定性测试
服务器连接状态下方提供“一键服务器稳定性测试”。该功能固定执行 100 轮,每轮都新建独立的 OnecardWebServiceClient 并调用一次 GetCustomerCount(appId);上一轮完成后等待 1000 毫秒再开始下一轮,最后一轮结束后不等待。启动前会快照页面当前 WebService 地址和 AppID,并清除普通连接的本地状态。
单轮成功必须同时满足 HTTP 请求完成、SOAP 不含 Fault、用户总数为非负整数。HTTP、超时、XML、SOAP Fault 或返回值错误只记为该轮失败,测试继续执行后续轮次。运行中按钮变为“停止稳定性测试”;停止不会强行打断当前 HTTP 请求,而是在当前请求完成或超时后生成部分报告。
页面实时显示完成轮数、成功数、失败数和本轮毫秒耗时。最终摘要包含计划/实际轮数、成功率、最短/最长/平均/P95 成功耗时、开始/结束时间、总耗时、停止状态和错误分组。P95 使用 nearest-rank 计算;没有成功样本时明确显示“无成功样本”。
完整逐轮报告使用 UTF-8 保存到应用专属文件目录的 server_stability 子目录,文件名为 server-stability-yyyy-MM-dd-HH-mm-ss-SSS.log;页面显示报告绝对路径。报告包含服务器地址和 AppID,但不记录注册码、密码摘要、证件号或卡片敏感信息。该功能仅用于短时现场验收,不是生产环境每秒重连机制。
4. 查询单点功能
除表中“附加必填字段”外,当前页面实现的查询请求都隐式携带连接时填写的 appId。SOAP 元素名区分大小写。
| 页面功能 | SOAP 操作 | 附加必填字段 | 页面校验 | 页面结果 |
|---|---|---|---|---|
| 获取单个用户信息 | GetInfoByCustID |
账号 CustomerID |
非空十进制 long |
解析后的中文字段列表;不显示密码摘要和证件号 |
| 获取部分用户信息 | GetCustomerByNum |
起始账号 custid、数量 num |
起始账号非负;数量大于 0 且不超过 Integer.MAX_VALUE |
原始 SOAP XML |
| 获取用户总数 | GetCustomerCount |
无 | 返回值必须是非负整数 | 数量文本 |
| 获取全部用户信息 | GetCustomerCount + 多次 GetCustomerByNum |
无 | 当前 Android 自动分页每批 1000 条,最后一批按剩余数量;分页值来自 Constants.ONE_CARD_CUSTOMER_BATCH_SIZE,是当前实现策略,不是厂家文档规定的服务端硬上限;从返回的最大 CUSTOMERID + 1 继续 |
每批提交后的保存进度;完成后显示同步 ID、服务器总数、保存数、批次数、SQLite 用户数和耗时 |
| 获取黑名单总数 | GetBLLTCount |
无 | 返回值必须是非负整数 | 数量文本 |
| 获取全部黑名单 | GetBLLTAll |
无 | 无额外输入 | 原始 SOAP XML |
| 获取部门总数 | GetCustDeptCount |
无 | 返回值必须是非负整数 | 数量文本 |
| 获取全部部门信息 | GetCustDeptInfo |
无 | 无额外输入 | 原始 SOAP XML |
HTTP 连接超时为 8 秒,读取超时为 15 秒。HTTP 非 2xx、空响应、SOAP Fault、数量格式错误和分段用户响应缺少 CUSTOMERID 都会显示为失败。
单个用户结果解析并展示以下服务端字段:CUSTOMERID、NAME、OUTID、CARDNO、CARDSN、SCARDSNR、STATUS、CARDTYPE、CUSTDEPT、ODDFARE、SUBODDFARE、OPCOUNT、OPENDT、NOUSEDATE。日期中的 T 会转换为空格;金额保持服务端原始字符串,不自行换算单位。PWD、QUERYPWD、IDCARDNO 等认证或个人敏感字段不会进入页面结果。
4.1 全部用户 SQLite 保存规则
“获取全部用户信息”不再把所有 SOAP Envelope 聚合到内存。每个 GetCustomerByNum 响应解析成功后,立即使用独立 SQLite 事务保存;事务提交成功才更新页面进度并请求下一批。数据库文件为应用私有目录中的 onecard.db,当前使用三张表:
| 表 | 用途 | 唯一性/关键规则 |
|---|---|---|
customers |
用户主表和常用查询字段 | customer_id 主键,对应原始 CUSTOMERID;重复账号更新,不产生重复用户 |
customer_fields |
保存每个用户的全部原始 XML 字段 | (customer_id, field_name) 联合主键,字段名不区分大小写;更新用户时替换其全部旧字段 |
customer_sync_runs |
保存同步开始、完成、数量、批次、状态和错误 | sync_id 主键;状态为 RUNNING、SUCCESS 或 FAILED |
当前现场响应已观察到 51 个字段,代码按字段名和值原样保存,并兼容服务端后续增加的未知字段。按用户要求,PWD、QUERYPWD、IDCARDNO 也会落库;这些敏感字段不显示到页面、不写日志、不写外部存储。Manifest 已关闭系统自动备份,但当前 SQLite 是应用私有目录中的明文数据库,不等同于数据库加密。
每批内部任何用户解析或插入失败时整批回滚,之前已经提交的批次保留。只有服务器总数对应的全部批次均成功,才删除本轮未出现的旧用户并标记同步 SUCCESS;中途失败只标记 FAILED,不清理旧用户。页面失败摘要显示此前成功保存的批次数和数量,不显示用户原始字段值。
4.2 全量同步分阶段耗时日志
全量同步复用应用现有 ReaderLog,日志写入应用私有目录 files/reader_logs/,按天生成、单文件最大5MB并保留最近7天,可从“查看读卡日志”页面查看和复制。该功能不新增数据库表,不把耗时明细写入 onecard.db,不增加页面控件。
耗时使用 System.nanoTime() 单调时钟计算,单位统一为毫秒。系统时间调整不会影响阶段耗时;结果为 0 表示阶段不足1毫秒。
| 日志事件 | 关键字段 | 含义 |
|---|---|---|
CUSTOMER_SYNC_START |
syncId、batchSize |
全量同步开始 |
CUSTOMER_SYNC_COUNT_SUCCESS |
expectedCount、requestMs |
获取服务器用户总数成功及请求耗时 |
CUSTOMER_SYNC_BATCH_START |
batch、startingCustomerId、requestedCount |
单批开始及分页定位 |
CUSTOMER_SYNC_REQUEST_SUCCESS |
batch、requestMs、responseBytes |
分页 SOAP 请求耗时及原始响应 UTF-8 字节数 |
CUSTOMER_SYNC_PARSE_SUCCESS |
batch、records、parseMs |
XML 解析条数与耗时 |
CUSTOMER_SYNC_SAVE_SUCCESS |
batch、batchSaved、totalSaved、saveMs、batchMs |
SQLite 批事务保存耗时及本批总耗时 |
CUSTOMER_SYNC_SUCCESS |
savedCount、batches、databaseCount、elapsedMs |
整轮同步成功摘要 |
CUSTOMER_SYNC_FAILURE |
stage、batch、savedCount、successfulBatches、stageMs、elapsedMs |
失败阶段和失败前已完成进度;stage 为 count/request/parse/save/complete |
日志只记录同步 ID、批次号、起始账号、数量、字节数、耗时和异常类型,不记录 SOAP 原文、姓名、卡号、证件号、PWD、QUERYPWD 或其他用户字段值。日志写入异常会被隔离,不改变同步成功或失败结果。
5. 消费记录上传
5.1 调用
- SOAP 操作:
UploadBusinessRec - 顶层必填参数:
dto。 dto内包含连接时的AppID,并包含页面提供的 16 个必填输入;数值允许填0,但不得留空。- 厂商
UPLOADINFO.rtn是输出字段,不是页面输入。
5.2 页面、厂商字段和 SOAP DTO 的 1:1 映射
| 页面字段 | 控件 ID | 文档/C 字段 | SOAP DTO 元素 | 必填 | 页面/代码类型与校验 |
|---|---|---|---|---|---|
| 终端编号 | input_upload_terminal_id |
termID |
TermID |
是 | 十进制 long,不得留空 |
| 账号 | input_upload_customer_id |
custID |
CustomerID |
是 | 十进制 long,不得留空 |
| 卡号 | input_upload_card_number |
cardNO |
CardNO |
是 | 十进制 long,不得留空 |
| 持卡序号 | input_upload_card_serial |
cardSn |
Cardsnr |
是 | 十进制 long,不得留空 |
| 卡类型 | input_upload_card_class |
cardClass |
TradeCardType |
是 | 十进制 long;文档值 4=M1、8=CPU |
| 用户卡应用序列号 | input_upload_card_asn |
cardASN |
CardSN |
是 | 非空字符串 |
| 总额 | input_upload_total_amount |
sumFare |
SumFare |
是 | 十进制 long,单位分 |
| 管理费 | input_upload_management_fee |
mngFare |
MngFare |
是 | 十进制 long,单位分,可填 0 |
| 钱包卡类型 | input_upload_wallet_type |
objNo |
PurseSector |
是 | 十进制 long;0=主钱包、1=补助钱包 |
| 消费额 | input_upload_consumption_amount |
opFare |
OpFare |
是 | 十进制 long,单位分 |
| 扣款后余额 | input_upload_balance |
oddFare |
OddFare |
是 | 十进制 long,单位分 |
| 扣款后消费计数 | input_upload_operation_count |
opCount |
Opcount |
是 | 十进制 long |
| 消费时间 | input_upload_consumption_time |
jyDT |
OPDT |
是 | 必须严格匹配 yyyy-MM-dd HH:mm:ss |
| PSAM 卡号 | input_upload_psam_id |
psamID |
SAMCardNO |
是 | 仅十进制数字字符串;本地消费结果同时提供可直接填写的十进制值 |
| PSAM 卡脱机交易序号 | input_upload_psam_sequence |
psamJyNo |
SAMTradeNO |
是 | 十进制 long |
| 交易验证码 | input_upload_tac |
tac |
CardMac |
是 | 十进制 long;若页面结果是十六进制 TAC,上传前需转成无符号十进制 |
当前上传页面不会自动带入本地消费结果,也不会自动上传;这是本阶段“单点功能、不串业务流程”的既定边界。
6. 实现文件索引
| 范围 | 文件 |
|---|---|
| 本地消费页面 | MainActivity.java、activity_main.xml |
| 消费编排与结果 | NewcapecUsbCardReader.java、ConsumptionRequest.java、ConsumptionResult.java、ConsumptionBatchResult.java |
| 消费协议 | NewcapecProtocol.java、ConsumptionApdu.java、HidFrameCodec.java |
| 服务器页面 | ServerOperationsActivity.java、activity_server_operations.xml |
| SOAP 客户端 | OnecardWebServiceClient.java、SoapRequestFactory.java、SoapResponse.java、CustomerInfoFormatter.java |
| 全部用户解析与同步 | CustomerRecord.java、CustomerBatchParser.java、CustomerPageSource.java、CustomerSyncService.java、CustomerSyncResult.java |
| 用户 SQLite 持久化 | CustomerRepository.java、SQLiteCustomerRepository.java |
| 全量同步耗时日志 | SyncLogger.java、ReaderLogSyncLogger.java、CustomerSyncService.java、ReaderLog.java、ReaderLogStore.java |
| 服务器稳定性测试 | ServerStabilityRunner.java、ServerStabilityRound.java、ServerStabilityResult.java、ServerStabilityReportFormatter.java、ServerStabilityReportStore.java |
| 上传模型 | UploadRecord.java |
7. 现场验收顺序
- Android 主钱包执行 1 分消费,核对余额、计数、PSAM 序号和 TAC。
- 使用有补助余额的 CPU 卡分别验证补助钱包和补贴优先跨钱包交易。
- 使用真实
.asmx地址和 AppID 连接服务器,逐个验证 8 个查询按钮。 - 点击“获取全部用户信息(分段)”,核对服务器总数、本轮保存数、SQLite 用户数相同,并确认同步状态为成功。
- 执行一键服务器稳定性测试,核对 100 轮进度、成功率、P95、错误汇总和本地报告。
- 使用服务器中已登记的终端、用户和卡数据上传一条测试记录,核对服务端
rtn与后台流水。