236 changed files with 19481 additions and 4311 deletions
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 774 KiB |
|
Before Width: | Height: | Size: 904 KiB |
@ -0,0 +1,170 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-CN"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>Voitrans应用用户协议</title> |
||||
|
<style> |
||||
|
body { |
||||
|
font-family: 'Arial', sans-serif; |
||||
|
line-height: 1.6; |
||||
|
margin: 0 auto; |
||||
|
max-width: 800px; |
||||
|
padding: 20px; |
||||
|
color: #333; |
||||
|
} |
||||
|
h1 { |
||||
|
text-align: center; |
||||
|
margin-bottom: 30px; |
||||
|
} |
||||
|
h2 { |
||||
|
margin-top: 30px; |
||||
|
border-bottom: 1px solid #eee; |
||||
|
padding-bottom: 10px; |
||||
|
} |
||||
|
p { |
||||
|
margin-bottom: 15px; |
||||
|
} |
||||
|
.indent { |
||||
|
padding-left: 20px; |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<h1>Voitrans应用用户协议</h1> |
||||
|
|
||||
|
<p>这些使用条款和条件告诉您如何使用Voitrans应用程序和在线服务("服务")进行访问。深圳市良闯时代科技有限公司(以下简称"良闯")在此特别提醒用户认真阅读、充分理解本《用户协议》(以下简称"本协议")。</p> |
||||
|
|
||||
|
<p>用户应认真阅读、充分理解本协议中各条款。请您审慎阅读并选择接受或不接受本协议。您使用Voitrans APP(以下简称"本APP"或"本软件")等行为将视为对本协议的接受,并同意接受本协议各项条款的约束。如您不同意本协议,请勿使用本APP。良闯有权修订本协议,更新后的协议条款将公布于本APP,自公布之日起生效。</p> |
||||
|
|
||||
|
<h2>一、总则</h2> |
||||
|
<p>1.1 本APP是良闯提供的用于连接良闯及其合作公司发布的耳机产品的软件,本软件及相关服务的所有权和运营权均归良闯所有。</p> |
||||
|
|
||||
|
<p>1.2 本协议是您(以下简称"用户")与良闯及其合作公司以及运营合作单位(以下简称"合作单位")之间关于用户使用本APP及相关服务所订立的协议。</p> |
||||
|
|
||||
|
<p>1.3 本软件及服务包括:</p> |
||||
|
<p class="indent">1.3.1 提供的软件为使用Voitrans的用户提供Voitrans的连接与绑定、功能设置管理等软件服务,具体以本APP 实际提供的软件服务为准。</p> |
||||
|
<p class="indent">1.3.2 本软件的更新、升级以及其他服务。</p> |
||||
|
|
||||
|
<h2>二、软件授权范围</h2> |
||||
|
<p>2.1 良闯就本软件给予用户一项个人的、不可转让、不可转授权以及非独占性的许可。</p> |
||||
|
|
||||
|
<p>2.2 用户可以为非商业目的在单一台移动终端设备上安装、使用、显示、运行本软件。但用户不得为商业运营目的安装、使用、运行本软件,不可以对本软件或者本软件运行过程中释放到任何终端设备内存中的数据及本软件运行过程中客户端与服务器端的交互数据进行复制、更改、修改、挂接运行或创作任何衍生作品,形式包括但不限于使用插件、外挂或非经授权的第三方工具/服务接入本软件和相关系统。如果需要进行商业性的销售、复制和散发,例如软件预装和捆绑,必须获得良闯的书面授权和许可。</p> |
||||
|
|
||||
|
<p>2.3 用户不得未经良闯许可,将本软件安装在未经良闯明示许可的其他终端设备上,包括但不限于电视、数字机顶盒、游戏机等装置。</p> |
||||
|
|
||||
|
<p>2.4 除本协议明示授权外,良闯未授权给用户其他权利,若用户使用其他权利时须另外取得良闯的书面同意。</p> |
||||
|
|
||||
|
<h2>三、软件升级</h2> |
||||
|
<p>3.1 用户必须选择适合所安装之终端设置的软件版本。否则,任何软件版本与装置机型不适合所引起的软件问题、装置问题或损坏,应完全由用户自行负责。</p> |
||||
|
|
||||
|
<p>3.2 为了改善用户体验、完善服务内容,良闯有权不时地为您提供本软件替换、修改、升级版本,本软件为用户默认开通"升级提示"功能,视用户使用的软件版本差异,良闯提供给用户自行选择是否需要开通此功能。软件新版本发布后,良闯不保证旧版本软件的继续可用。</p> |
||||
|
|
||||
|
<h2>四、使用规范</h2> |
||||
|
<p>4.1 用户在遵守法律及本协议的前提下可依本协议使用本软件及服务,用户不得实施如下行为:</p> |
||||
|
|
||||
|
<p class="indent">4.1.1 删除本软件及其他副本上一切关于版权的信息,以及修改、删除或避开本软件为保护知识产权而设置的技术措施。</p> |
||||
|
|
||||
|
<p class="indent">4.1.2 对本软件进行反向工程,如反汇编、反编译或者其他试图获得本软件的源代码。</p> |
||||
|
|
||||
|
<p class="indent">4.1.3 通过修改或伪造软件运行中的指令、数据,增加、删减、变动软件的功能或运行效果,或者将用于上述用途的软件、方法进行运营或向公众传播,无论这些行为是否为商业目的。</p> |
||||
|
|
||||
|
<p class="indent">4.1.4 使用本软件进行任何危害网络安全的行为,包括但不限于:使用未经许可的数据或进入未经许可的服务器/账户;未经允许进入公众网络或者他人操作系统并删除、修改、增加存储信息;未经许可企图探查、扫描、测试本软件的系统或网络的弱点或其它实施破坏网络安全的行为;企图干涉、破坏本软件系统或网站的正常运行,故意传播恶意程序或病毒以及其他破坏干扰正常网络信息服务的行为;伪造 TCP/IP 数据包名称或部分名称。</p> |
||||
|
|
||||
|
<p class="indent">4.1.6 未经良闯书面同意,用户对软件及其中的信息擅自实施包括但不限于下列行为:使用、出租、出借、复制、修改、链接、转载、汇编、发表、出版,建立镜像站点、擅自借助本软件发展与之有关的衍生产品、作品、服务、插件、外挂、兼容、互联等。</p> |
||||
|
|
||||
|
<p class="indent">4.1.7 利用本软件发表、传送、传播、储存违反您当地法律的内容。</p> |
||||
|
|
||||
|
<p class="indent">4.1.8 利用本软件发表、传送、传播、储存侵害他人知识产权、商业秘密等合法权利的内容。</p> |
||||
|
|
||||
|
<p class="indent">4.1.9 利用本软件批量发表、传送、传播广告信息及垃圾信息。</p> |
||||
|
|
||||
|
<p class="indent">4.1.10 其他以任何不合法的方式、为任何不合法的目的、或以任何与本协议许可使用不一致的方式使用本软件和良闯提供的其他服务。</p> |
||||
|
|
||||
|
<p>4.2 信息发布规范</p> |
||||
|
|
||||
|
<p class="indent">4.2.1 您使用本软件发表信息内容的(如有),您应保证,您拥有您所上传信息内容的知识产权或已获得合法授权,您使用本软件及服务的任何行为未侵犯任何第三方之合法权益。</p> |
||||
|
|
||||
|
<p class="indent">4.2.2 用户在使用本软件时不得利用本软件从事以下行为,包括但不限于:</p> |
||||
|
|
||||
|
<p class="indent">4.2.2.1 制作、复制、发布、传播、储存违反当地国家法律法规的内容。</p> |
||||
|
|
||||
|
<p class="indent">4.2.2.2 发布、传送、传播、储存侵害他人名誉权、肖像权、知识产权、商业秘密等合法权利的内容。</p> |
||||
|
|
||||
|
<p class="indent">4.2.2.3 虚构事实、隐瞒真相以误导、欺骗他人。</p> |
||||
|
|
||||
|
<p class="indent">4.2.2.4 发表、传送、传播广告信息及垃圾信息。</p> |
||||
|
|
||||
|
<p class="indent">4.2.2.5 从事其他违反当地法律法规的其他行为。</p> |
||||
|
|
||||
|
<p class="indent">4.2.3 未经良闯许可,您不得在本软件中进行任何诸如发布广告、销售商品的商业行为。</p> |
||||
|
|
||||
|
<p>4.3 用户理解并同意:</p> |
||||
|
|
||||
|
<p class="indent">4.3.1 良闯会对用户是否涉嫌违反上述使用规范做出认定,并根据认定结果中止、终止对您的使用许可或采取其他依本约定可采取的限制措施。</p> |
||||
|
|
||||
|
<p class="indent">4.3.2 对于用户使用许可软件时发布的涉嫌违法或涉嫌侵犯他人合法权利或违反本协议的信息,良闯会直接删除。</p> |
||||
|
|
||||
|
<p class="indent">4.3.3 对于用户违反上述使用规范的行为对第三方造成损害的,您需要以自己的名义独立承担法律责任,并应确保良闯免于因此产生损失或增加费用。</p> |
||||
|
|
||||
|
<p class="indent">4.3.4 若用户违反有关法律规定或协议约定,使良闯遭受损失,或受到第三方的索赔,或受到行政管理机关的处罚,用户应当赔偿良闯因此造成的损失和(或)发生的费用,包括合理的律师费、调查取证费用。</p> |
||||
|
|
||||
|
<h2>五、第三方服务说明</h2> |
||||
|
<p>5.1 用户理解并同意,本软件系统可能包含由良闯的关联方或第三方提供的服务,良闯只是为了用户便利操作而在系统中提供相关功能模块,提供第三方服务的使用入口。</p> |
||||
|
|
||||
|
<p>5.2 不论第三方服务预置于本软件系统服务中,还是由用户自行开通或订购,用户均理解并同意,良闯不对第三方服务提供方或用户行为的合法性、有效性,以及第三方服务的合法性、准确性、有效性、安全性进行任何明示或默示的保证或担保。</p> |
||||
|
|
||||
|
<p>5.3 良闯并不监督第三方服务,不对其拥有任何控制权,也不对第三方服务提供任何形式的保证或担保,更不承担任何责任。</p> |
||||
|
|
||||
|
<p>5.4 用户与第三方服务提供方之间发生的任何争议、纠纷应由用户与该方按照第三方服务提供方的业务流程及业务规范自行协商解决,良闯不承担任何责任。</p> |
||||
|
|
||||
|
<h2>六、隐私政策与个人信息保护</h2> |
||||
|
<p>6.1 良闯承诺按照隐私政策保护用户的隐私,具体请见:《Voitrans应用隐私政策》。</p> |
||||
|
|
||||
|
<h2>七、违约责任</h2> |
||||
|
<p>7.1 良闯有权判断用户的行为是否符合本协议条款规定,如果认为用户违反有关法律法规或者本协议、相关规则的规定,根据用户违规情形的严重程度,良闯有权对用户采取删除违规信息、限制、中止、终止用户使用本软件服务、追究用户的法律责任以及其他良闯认为适合的处理措施。如果使良闯遭受任何损失(包括但不限于收到任何第三方的索赔或任何行政管理部门的处罚),用户应当承担全部责任。</p> |
||||
|
|
||||
|
<h2>八、服务风险及免责声明</h2> |
||||
|
<p>8.1 良闯以目前的技术提供服务支持。良闯不保证本软件服务在操作上不会中断或没有错误,不保证会纠正本软件服务的所有缺陷,亦不保证本软件服务能满足用户的所有要求。由此产生的后果,良闯不承担任何责任。</p> |
||||
|
|
||||
|
<p>8.2 用户因第三方如通讯线路故障、技术问题、网络、电脑终端设备故障、系统不稳定性及其他各种不可抗力原因而遭受的一切损失,良闯不承担责任。</p> |
||||
|
|
||||
|
<p>8.3 良闯不保证通过本软件获得的信息内容(包括但不限于调用的第三方服务内容)的合法性、真实性、准确性、有效性,良闯不对用户基于查询、查看做出的任何行为的结果承担任何责任。</p> |
||||
|
|
||||
|
<p>8.4 本软件同大多数互联网软件一样,受包括但不限于用户原因、网络服务质量、社会环境等因素的差异影响,可能受到各种安全问题的侵扰,如他人利用用户的资料,造成现实生活中的骚扰;用户下载安装的其他软件或访问的其他网站中含有"特洛伊木马"等病毒,威胁到用户的终端设备信息和数据的安全,继而影响本软件的正常使用等等。用户应加强信息安全及使用者资料的保护意识,要注意加强密码保护,以免遭致损失和骚扰。</p> |
||||
|
|
||||
|
<p>8.5 因用户使用本软件或要求良闯提供特定服务时,本软件可能会调用第三方系统或第三方软件支持用户的使用或访问,使用或访问的结果由该第三方提供,用户除遵守本协议相关规则外,还应遵守第三方的协议、相关规则。用户须理解并同意,在使用第三方服务时,第三方可能会对用户数据进行读取,良闯不保证通过第三方系统或第三方软件支持实现的结果的安全性、准确性、有效性及其他不确定的风险,用户应审慎判断,由此若引发的任何争议及损害,良闯不承担任何责任。</p> |
||||
|
|
||||
|
<p>8.6 良闯特别提请用户注意,良闯为了保障公司业务发展和调整的自主权,良闯采取合理的方式通知用户后可随时修改或中断服务。</p> |
||||
|
|
||||
|
<p>8.7 除法律法规有明确规定外,良闯将尽最大努力确保软件及其所涉及的技术及信息安全、有效、准确、可靠,但受限于现有技术,用户理解良闯不能对此进行担保。</p> |
||||
|
|
||||
|
<p>8.8 由于用户因下述任一情况所引起或与此有关的人身伤害或附带的、间接的经济损害赔偿,包括但不限于利润损失、资料损失、业务中断的损害赔偿或其他商业损害赔偿或损失,需由用户自行承担:</p> |
||||
|
|
||||
|
<p class="indent">(1)使用或未能使用许可软件。</p> |
||||
|
|
||||
|
<p class="indent">(2)第三方未经许可的使用软件或更改用户的数据。</p> |
||||
|
|
||||
|
<p class="indent">(3)用户使用软件进行的行为产生的费用及损失。</p> |
||||
|
|
||||
|
<p class="indent">(4)用户对软件的误解。</p> |
||||
|
|
||||
|
<p class="indent">(5)非因良闯的原因引起的与软件有关的其他损失。</p> |
||||
|
|
||||
|
<p>8.9 用户同意良闯有权基于司法、监管部门、监督机构的要求或自身业务原因,暂停、中断或终止向用户提供全部或者部分本服务。对此良闯不承担任何责任。</p> |
||||
|
|
||||
|
<p>8.10 用户理解并同意,良闯将会尽其商业上的合理努力保障用户在本软件及服务中的数据存储安全,但良闯不能提供完全保证,用户须理解,良闯不对服务中的数据的删除或储存或备份失败负责。</p> |
||||
|
|
||||
|
<p>8.11 在法律允许的最大范围内,开发者或供应商不对因使用应用程序而造成的任何直接、间接、偶然、特殊或后果性的损害负责。用户如有进一步的需要,请去往正规医院进行深度检测。</p> |
||||
|
|
||||
|
<h2>九、知识产权声明</h2> |
||||
|
<p>9.1 良闯是本软件的知识产权权利人。本软件的一切著作权、商标权、专利权、商业秘密等知识产权,以及与本软件相关的所有信息内容(包括但不限于文字、图片、音频、视频、图表、界面设计、版面框架、有关数据或电子文档等)均受当地法律法规和相应的国际条约保护,良闯享有上述知识产权。</p> |
||||
|
|
||||
|
<p>9.2 未经良闯书面同意,用户不得为任何商业或非商业目的自行或许可任何第三方实施、利用、转让上述知识产权,良闯保留追究上述行为法律责任的权利。</p> |
||||
|
|
||||
|
<h2>十、协议变更</h2> |
||||
|
<p>10.1 良闯有权在必要时修改本协议条款,协议条款一旦发生变动,将会在相关页面上公布修改后的协议条款。如果不同意所改动的内容,用户应主动取消此项服务。如果用户继续使用服务,则视为接受协议条款的变动。</p> |
||||
|
|
||||
|
<p>10.2 良闯有权按需要修改或变更所提供的收费服务、收费标准、收费方式、服务费及服务条款。良闯在提供服务时,可能现在或日后对部分服务的用户开始收取一定的费用如用户拒绝支付该等费用,则不能在收费开始后继续使用相关的服务。</p> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,151 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>Voitrans Application User Protocol</title> |
||||
|
<style> |
||||
|
body { |
||||
|
font-family: "Calibri", "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; |
||||
|
line-height: 1.6; |
||||
|
color: #333; |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
background-color: #fff; |
||||
|
} |
||||
|
.container { |
||||
|
max-width: 800px; |
||||
|
margin: 0 auto; |
||||
|
padding: 20px 40px; |
||||
|
} |
||||
|
.header-info { |
||||
|
text-align: right; |
||||
|
font-size: 14px; |
||||
|
color: #888; |
||||
|
margin-bottom: 30px; |
||||
|
} |
||||
|
h2 { |
||||
|
font-size: 22px; |
||||
|
font-weight: bold; |
||||
|
text-align: center; |
||||
|
margin-bottom: 24px; |
||||
|
color: #333; |
||||
|
} |
||||
|
h3 { |
||||
|
font-size: 18px; |
||||
|
font-weight: bold; |
||||
|
margin-top: 24px; |
||||
|
margin-bottom: 12px; |
||||
|
color: #333; |
||||
|
} |
||||
|
p { |
||||
|
margin-bottom: 12px; |
||||
|
text-align: justify; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
/* Responsive adjustments */ |
||||
|
@media (max-width: 600px) { |
||||
|
.container { |
||||
|
padding: 15px; |
||||
|
} |
||||
|
h2 { |
||||
|
font-size: 20px; |
||||
|
} |
||||
|
h3 { |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div class="container"> |
||||
|
<div class="header-info">English(US)</div> |
||||
|
|
||||
|
<h2>Voitrans Application User Protocol</h2> |
||||
|
|
||||
|
<p>These Terms and Conditions of Use tell you how to access using the Voitrans Application and online services (the "Services"). Saitong Industries Inc. (hereinafter referred to as "Saitong") hereby reminds users to carefully read and fully understand this User Agreement (hereinafter referred to as the "Agreement"). Users should carefully read and fully understand the terms of this Agreement. Please read carefully and choose to accept or not accept this Agreement. Your use of Voitrans APP (hereinafter referred to as "the APP" or" the Software ") will be deemed as acceptance of this Agreement and you agree to be bound by the terms of this Agreement. If you do not agree to this Agreement, do not use the APP.</p> |
||||
|
<p>Saitong reserves the right to amend this Agreement, and the updated terms of the Agreement will be published in this APP, effective from the date of publication.</p> |
||||
|
|
||||
|
<h3>1. General rules</h3> |
||||
|
<p>1.1 This APP is the software provided by Saitong for connecting the headset products released by Saitong and its partner companies. The ownership and operation rights of the software and related services are all owned by Saitong.</p> |
||||
|
<p>1.2 This Agreement is an agreement between you (hereinafter referred to as the "User") and the company and its partners and operating partners (hereinafter referred to as the "Partners") regarding the user's use of the APP and related services.</p> |
||||
|
<p>1.3 The Software and services include:</p> |
||||
|
<p>1.3.1 The software provided provides Voitrans users with Voitrans connection and binding, function setting management and other software services, subject to the actual software services provided by the APP.</p> |
||||
|
<p>1.3.2 Updates, upgrades and other services of the software.</p> |
||||
|
|
||||
|
<h3>2. Scope of software license</h3> |
||||
|
<p>2.1 We grant the user a personal, non-transferable, non-sublicensable, and non-exclusive license to the Software.</p> |
||||
|
<p>2.2 Users can install, use, display and run the software on a single mobile terminal device for non-commercial purposes. However, the user shall not install, use or run the software for commercial operation purposes, copy, change, modify, connect or run the software or the data released into the memory of any terminal device during the operation of the software or the interaction data between the client and the server during the operation of the software, or create any derivative works. This includes, but is not limited to, the use of plug-ins, plug-ins, or unauthorized third-party tools/services to access the Software and related systems. For commercial sale, reproduction and distribution, such as software pre-installation and bundling, written authorization and permission must be obtained from the idea.</p> |
||||
|
<p>2.3 The User shall not install the Software on other terminal devices without the express permission of the Creator, including but not limited to TVS, digital set-top boxes, game consoles and other devices.</p> |
||||
|
<p>2.4 Except as expressly authorized by this Agreement, no other rights are granted to the User, and the User shall obtain the written consent of the User to use other rights.</p> |
||||
|
|
||||
|
<h3>3. Software upgrade</h3> |
||||
|
<p>3.1 The user must select the software version suitable for the setup of the installed terminal. Otherwise, software problems, device problems or damage caused by any software version not suitable for the device type shall be solely the responsibility of the user.</p> |
||||
|
<p>3.2 In order to improve the user experience and improve the service content, Saitong has the right to provide you with replacement, modification and upgrade versions of the software from time to time. The "Upgrade prompt" function is enabled for users by default. Depending on the different software versions used by users, Saitong provides users with the option to activate this function. After the release of a new version of the software, Saitong does not guarantee the continued availability of the old version of the software.</p> |
||||
|
|
||||
|
<h3>4. Usage specification</h3> |
||||
|
<p>4.1 Users may use the Software and Services in accordance with this Agreement under the premise of abiding by the law and this Agreement, and users shall not perform the following acts:</p> |
||||
|
<p>4.1.1 Delete all copyright information on the Software and other copies, and modify, delete or circumvent the technical measures set up by the Software to protect intellectual property rights.</p> |
||||
|
<p>4.1.2 Reverse engineering the Software, such as disassembly, decompilation, or other attempts to obtain the source code of the Software.</p> |
||||
|
<p>4.1.3 By modifying or falsifying instructions and data in the operation of the software, increasing, deleting or changing the function or operation effect of the software, or software or methods to be used for the above purposes to operate or disseminate to the public, regardless of whether these acts are for commercial purposes.</p> |
||||
|
<p>4.1.4 Use the software for any behavior that jeopardizes network security, including but not limited to: using unauthorized data or accessing unauthorized servers/accounts; Access public networks or other operating systems without permission and delete, modify or add stored information; Unauthorized attempts to probe, scan, or test the weaknesses of the system or network of the software or other acts that undermine network security; Attempts to interfere with or destroy the normal operation of the software system or website, intentionally spread malicious programs or viruses, and other acts that disrupt normal network information services; Forge TCP/IP packet names or partial names.</p> |
||||
|
<p>4.1.6 Without the written consent of the Company, the user arbitrarily performs the following acts on the software and the information in it, including but not limited to: using, renting, lending, copying, modifying, linking, reprinting, compiling, publishing, publishing, establishing mirror sites, developing derivative products, works, services, plug-ins, plug-ins, compatibility, interconnection, etc., with the help of the Software.</p> |
||||
|
<p>4.1.7 Use the Software to publish, transmit, distribute, and store content that violates your local laws.</p> |
||||
|
<p>4.1.8 Use the software to publish, transmit, disseminate, and store content that infringes others' intellectual property rights, trade secrets, and other legitimate rights.</p> |
||||
|
<p>4.1.9 Use this software to publish, transmit and disseminate advertising information and junk information in bulk.</p> |
||||
|
<p>4.1.10 Use the Software and other Services in any unlawful manner, for any unlawful purpose, or in any manner inconsistent with the licensed use of this Agreement.</p> |
||||
|
<p>4.2 Information release specification</p> |
||||
|
<p>4.2.1 If you use the software to publish information content (if any), you should guarantee that you own the intellectual property rights of the information content uploaded by you or have obtained legal authorization, and any behavior of you using the software and services does not infringe on the legitimate rights and interests of any third party.</p> |
||||
|
<p>4.2.2 When using the Software, the user shall not use the Software to engage in the following activities, including but not limited to:</p> |
||||
|
<p>4.2.2.1 Create, copy, publish, disseminate, and store content that violates local national laws and regulations.</p> |
||||
|
<p>4.2.2.2 Publish, transmit, disseminate, and store content that infringes others' legal rights such as reputation, portrait, intellectual property, and trade secrets.</p> |
||||
|
<p>4.2.2.3 Fabricate facts, conceal the truth in order to mislead or deceive others.</p> |
||||
|
<p>4.2.2.4 Publish, transmit and disseminate advertising information and junk information.</p> |
||||
|
<p>4.2.2.5 Engaging in other acts in violation of local laws and regulations.</p> |
||||
|
<p>4.2.3 You may not carry out any commercial activities such as advertising and selling goods in the Software without the permission of the Company.</p> |
||||
|
<p>4.3 The User understands and agrees that:</p> |
||||
|
<p>4.3.1 Saitong will determine whether the user is suspected of violating the above specifications of use, and according to the results of the determination, suspend or terminate your license to use or take other restrictive measures that can be taken in accordance with this agreement.</p> |
||||
|
<p>4.3.2 For the user's use of the licensed software published suspected of illegal or suspected infringement of the legal rights of others or violation of this Agreement, the information will be directly deleted.</p> |
||||
|
<p>4.3.3 If the user's violation of the above rules of use causes damage to third parties, you need to bear legal responsibility independently in your own name, and you should ensure that the idea is free from losses or increased costs.</p> |
||||
|
<p>4.3.4 If the user violates the relevant legal provisions or agreements, causing the Saitong to suffer losses, or is subject to third-party claims, or is punished by the administrative authority, the user shall compensate the loss and/or expenses caused by the Saitong, including reasonable attorney fees, investigation and evidence collection costs.</p> |
||||
|
|
||||
|
<h3>5. Third-party service description</h3> |
||||
|
<p>5.1 The user understands and agrees that the software system may contain services provided by affiliates of the Company or third parties, and that the Company only provides relevant functional modules in the system for the convenience of users and provides access to third-party services.</p> |
||||
|
<p>5.2 Whether the third-party services are preset in the software system services, or are opened or ordered by users themselves, users understand and agree that Saitong does not make any express or implied guarantee or guarantee of the legality and effectiveness of the third-party service providers or users, as well as the legality, accuracy, effectiveness and security of the third-party services.</p> |
||||
|
<p>5.3 The company does not supervise, control or provide any warranty or guarantee of any kind for, or assume any liability for, third party services.</p> |
||||
|
<p>5.4 Any dispute or dispute between the user and the third party service provider shall be resolved by the User and the third party in accordance with the business process and business norms of the third party service provider, and the company shall not assume any responsibility.</p> |
||||
|
|
||||
|
<h3>6. Privacy Policy and Protection of Personal Information</h3> |
||||
|
<p>6.1 Saitong is committed to protecting the privacy of users in accordance with the privacy policy, please see: Voitrans applies Privacy Policy.</p> |
||||
|
|
||||
|
<h3>7. Liability for breach</h3> |
||||
|
<p>7.1 Saitong has the right to judge whether the user's behavior complies with the provisions of this Agreement. If it is believed that the User has violated relevant laws and regulations or the provisions of this Agreement and relevant rules, according to the severity of the user's violation, Saitong has the right to delete the illegal information, restrict, suspend, terminate the user's use of the Software service, pursue the user's legal liability and take other measures that Saitong deems appropriate. If we suffer any loss (including but not limited to receiving any third party claims or penalties from any administrative department), the User shall be fully liable.</p> |
||||
|
|
||||
|
<h3>8. Service Risks and Disclaimers</h3> |
||||
|
<p>8.1 We want to provide service support with current technology. Saitong does not warrant that the Software Service will operate without interruption or error, that all defects of the Software Service will be corrected, or that the Software Service will meet all requirements of users. The resulting consequences, the Saitong does not assume any responsibility.</p> |
||||
|
<p>8.2 All losses suffered by the user due to third parties such as communication line failure, technical problems, network, computer terminal equipment failure, system instability and other force majeure causes shall not be held liable.</p> |
||||
|
<p>8.3 Saitong does not guarantee the legality, authenticity, accuracy and effectiveness of the information content obtained through the software (including but not limited to the third party service content), and Saitong does not assume any responsibility for the results of any behavior made by users based on query and viewing.</p> |
||||
|
<p>8.4 The software, like most Internet software, is affected by differences in factors including but not limited to user reasons, network service quality, social environment and other factors, and may be subjected to various security problems, such as others using user information, resulting in harassment in real life; Other software downloaded and installed by users or other websites visited by users contain viruses such as "Trojan horses", which threaten the security of users' terminal equipment information and data, and then affect the normal use of the software. Users should strengthen the awareness of information security and user data protection, and pay attention to strengthen password protection to avoid loss and harassment.</p> |
||||
|
<p>8.5 When users use the Software or require Saitong to provide specific services, the Software may call third-party systems or third-party software to support users' use or access, and the results of use or access are provided by the third party. In addition to complying with the relevant rules of this Agreement, users shall also comply with the agreements and relevant rules of the third party. Users must understand and agree that, when using third-party services, third parties may read user data, and Saitong does not guarantee the safety, accuracy, effectiveness and other uncertain risks of the results achieved through third-party systems or third-party software support, users should exercise careful judgment, and Saitong does not assume any responsibility for any disputes and damages caused by this.</p> |
||||
|
<p>8.6 In order to guarantee the autonomy of the company's business development and adjustment, the company may modify or interrupt the service at any time after taking a reasonable way to notify the user.</p> |
||||
|
<p>8.7 Except as expressly provided by laws and regulations, Saitong will make its best efforts to ensure that the software and the technology and information involved are safe, effective, accurate and reliable, but limited by the existing technology, the user understands that Saitong cannot guarantee this.</p> |
||||
|
<p>8.8 Compensation for personal injury or incidental and indirect economic damage caused by or in connection with any of the following circumstances, including but not limited to loss of profits, loss of data, damages for business interruption or other business damages or losses, shall be borne by the User:</p> |
||||
|
<p>1) Use or failure to use the licensed software.</p> |
||||
|
<p>2) Unauthorized use of the software or alteration of user data by third parties.</p> |
||||
|
<p>3) Expenses and losses incurred by the user's actions with the software.</p> |
||||
|
<p>4) User misconceptions about the software.</p> |
||||
|
<p>5) Any other loss related to the software that is not attributable to Saitong causes.</p> |
||||
|
<p>8.9 The User agrees that the Company has the right to suspend, interrupt or terminate all or part of the Service to the user based on the requirements of the judicial, regulatory and supervisory authorities or its own business reasons. Saitong accepts no responsibility for this.</p> |
||||
|
<p>8.10 The User understands and agrees that the Company will use its commercially reasonable efforts to ensure the security of data storage in the Software and Services, but the company cannot provide a complete guarantee, and the User understands that the Company is not responsible for the deletion or storage or backup failure of data in the Service.</p> |
||||
|
<p>8.11 To the fullest extent permitted by law, the Developer or Supplier shall not be liable for any direct, indirect, incidental, special or consequential damages arising from the use of the Application. If the user has further needs, please go to the regular hospital for in-depth testing.</p> |
||||
|
|
||||
|
<h3>9. Intellectual property statement</h3> |
||||
|
<p>9.1 Saitong is the intellectual property right holder of this software. All Copyrights, trademarks, patents, trade secrets and other intellectual property rights of the software, as well as all information content related to the software (including but not limited to text, pictures, audio, video, graphics, interface design, layout frame, relevant data or electronic documents, etc.) are protected by local laws and regulations and corresponding international treaties.</p> |
||||
|
<p>9.2 Without the written consent of the Company, the User shall not implement, utilize or transfer the above intellectual property rights for any commercial or non-commercial purpose, or permit any third party, and the Company reserves the right to pursue the legal responsibility for the above acts.</p> |
||||
|
|
||||
|
<h3>10. Protocol change</h3> |
||||
|
<p>10.1 Saitong has the right to modify the terms of this Agreement when necessary. Once the terms of the Agreement are changed, the revised terms of the Agreement will be published on the relevant page. If you do not agree with the changed content, the user should take the initiative to cancel this service. If the User continues to use the Services, it is deemed to accept the changes in the terms of the Agreement.</p> |
||||
|
<p>10.2 Saitong reserves the right to modify or change the paid services, charging standards, charging methods, service fees and terms of service provided as necessary. When providing services, we may charge certain fees to users of some services now or in the future. If users refuse to pay such fees, they cannot continue to use the relevant services after the start of the fees.</p> |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,397 @@ |
|||||
|
<html> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="utf-8" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> |
||||
|
<title>Voitrans隐私政策</title> |
||||
|
<style> |
||||
|
body { |
||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; |
||||
|
line-height: 1.6; |
||||
|
margin: 0; |
||||
|
padding: 16px; |
||||
|
color: #2d2d2d; |
||||
|
background-color: #fafafa; |
||||
|
overflow-x: hidden; |
||||
|
word-wrap: break-word; |
||||
|
word-break: break-all; |
||||
|
} |
||||
|
|
||||
|
@media (min-width: 768px) { |
||||
|
body { |
||||
|
max-width: 900px; |
||||
|
margin: 0 auto; |
||||
|
padding: 20px 40px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
h1 { |
||||
|
text-align: center; |
||||
|
font-size: 28px; |
||||
|
margin-top: 20px; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
h2 { |
||||
|
font-size: 24px; |
||||
|
margin-top: 18px; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
h3 { |
||||
|
font-size: 20px; |
||||
|
margin-top: 16px; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
h4 { |
||||
|
font-size: 16px; |
||||
|
margin-top: 12px; |
||||
|
font-weight: bold; |
||||
|
color: #555; |
||||
|
} |
||||
|
p { |
||||
|
margin-bottom: 1em; |
||||
|
text-align: justify; |
||||
|
word-wrap: break-word; |
||||
|
word-break: break-all; |
||||
|
} |
||||
|
ul { |
||||
|
margin-bottom: 1em; |
||||
|
padding-left: 20px; |
||||
|
} |
||||
|
ul li { |
||||
|
margin-bottom: 0.5em; |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<h1>Voitrans 隐私政策</h1> |
||||
|
<p>更新日期:2025年5月27日</p> |
||||
|
<p>生效日期:2025年5月27日</p> |
||||
|
<p>深圳市良闯时代科技有限公司(下称“我们”),是VoitransAPP(具体范围及定义见下文)的开发者和运营者。我们深知个人信息对您的重要性,您的信任对我们非常重要,我们将按照法律法规的规定并参照行业最佳实践保护您的个人信息及隐私安全。我们制定本"隐私政策"并特别提示:希望您在使用Voitrans及相关服务前仔细阅读并理解本隐私政策,以便做出适当的选择。 |
||||
|
</p> |
||||
|
<p>本隐私政策将帮助您了解 :</p> |
||||
|
<p>我们会遵循隐私政策收集、使用您的信息,但不会仅因您同意本隐私政策而采用强制捆绑的方式收集个人信息。</p> |
||||
|
<p>当您使用或开启相关功能或使用服务时,为实现功能、服务所必需,我们会收集、使用相关信息。除非是为实现业务功能或根据法律法规要求所必需的必要信息,您均可以拒绝提供且不影响其他功能或服务。我们将在隐私政策中逐项说明所要收集的信息。 |
||||
|
</p> |
||||
|
<p>如果您未登录账号,我们会通过设备对应的标识符信息来保障信息推送的基本功能。如果您登录了账号,我们会根据账号信息实现信息推送。</p> |
||||
|
<p>精确地理位置、摄像头(相机)、相册(存储)等权限,均不会默认开启,只有经过您的明示授权才会在为实现特定功能或服务时使用,您也可以撤回授权。特别需要指出的是,即使经过您的授权,我们获得了这些敏感权限,也不会在相关功能或服务不需要时而收集您的信息。 |
||||
|
</p> |
||||
|
<p>下文将帮您详细了解我们如何收集、使用、存储、转移(如适用)与保护个人信息;帮您了解查阅、复制、更正、删除、撤回授权个人信息的方式。本政策与您使用我们的服务关系密切,我们建议您仔细阅读并理解本政策全部内容,作出您认为适当的选择,有关您个人信息权益的条款重要内容我们已用加粗形式提示 |
||||
|
, 请特别关注。</p> |
||||
|
<p>1.我们如何收集和使用个人信息</p> |
||||
|
<p>2 .数据使用过程中涉及的合作方以及转移、公开个人信息</p> |
||||
|
<p>3 .我们如何存储个人信息</p> |
||||
|
<p>4 .我们如何保护个人信息的安全</p> |
||||
|
<p>5 .我们如何管理您的个人信息</p> |
||||
|
<p>6 .未成年人条款</p> |
||||
|
<p>7 .隐私政策的修订和通知</p> |
||||
|
<p>8 .联系我们</p> |
||||
|
<h2>1. 我们如何收集和使用个人信息</h2> |
||||
|
<p>我们会按照如下方式收集您在访问Voitrans平台或使用Voitrans平台提供的产品及服务时主动提供的,以及通过自动化手段收集您在使用功能或接受服务过程中产生的信息,收集和使用这些信息的目的和用途详见下述功能描述部分。 |
||||
|
</p> |
||||
|
<p>我们不会通过共享、转让、搜集公开信息等其他渠道间接获取有关您的个人信息。如将来发生需通过其他渠道间接获取您的个人信息时,我们只获取实现产品或服务的业务功能所必需的最少数量的信息,且在收集和使用之前明确告知您并征求您的明示同意。同时,我们将在收集信息前确保信息来源的合法性,了解个人信息提供方已获得的个人信息处理的授权同意范围,包括使用目的、授权同意转让、共享、公开披露等。若我们开展业务需进行的个人信息处理活动超出该授权同意范围,会在获取个人信息后的合理期限内或处理个人信息前,征得您的明示同意。 |
||||
|
</p> |
||||
|
<p>我们收集和使用您的信息的目的和用途详情如下:</p> |
||||
|
<h3>1.1 注册、登录Voitrans</h3> |
||||
|
<p>当您注册、登录Voitrans时,您可以通过手机号,电子邮箱,以及微信授权方式,创建“Voitrans账号”,并且完善相关的网络身份识别信息(账号、密码、头像、昵称),我们会收集您的手机号,并且获取短信验证码,以便我们对您的账户进行身份核验。 |
||||
|
</p> |
||||
|
<p>收集您的上述信息是为了帮助您完成手机号注册和登陆,保证您的账户权益。此类信息属于使用手机号注册和登录的必要信息,如您不提供将无法完成注册/登录操作。</p> |
||||
|
<h3>1.2 录音</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.2.1 必要信息范围</h4> |
||||
|
<p>在您联网使用录音功能服务时,我们将收集您的语音信息与/或您提供的其他录音内容 。此类信息属于录音功能服务的必要信息 。请您知悉,在未联网的环境下或您未主动上传的情况下,前述信息仅会存储在设备端 |
||||
|
,我们不会收集并上传云端服务器存储。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.2.2 调用的权限范围</h4> |
||||
|
<p>在您使用录音功能服务时,我们需要调用您的麦克风权限以获取您的语音信息,此为该功能服务所必须的系统权限,若您拒绝,您将无法使用录音功能 。</p> |
||||
|
<p>在您使用录音存储功能服务时,我们需要调用您的存储权限以存储您的语音信息,此为该功能服务所必须的系统权限,若您拒绝,您将无法使用录音功能 。</p> |
||||
|
<h3>1.3 语音转写</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.3.1 必要信息范围</h4> |
||||
|
<p>在您使用语音转文字功能服务时,我们将收集您的语音信息与/或您提供的其他录音内容以在云端服务器进行语音转写。同时,我们会收集转写后的文本信息 |
||||
|
,判断您的需求并做出实时处理反馈给您,以方便您了解进行了哪些语音交互。此类信息属于语音转文字功能的必要信息,如您不能提供,您将无法使用语音转文字功能。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.3.2 调用的权限范围</h4> |
||||
|
<p>在您使用语音转文字功能服务时,我们需要调用您的麦克风权限以获取您的语音信息,此为该功能服务所必须的系统权限,若您拒绝,您将无法使用语音转文字功能。</p> |
||||
|
<h3>1.4 管理录音及转写文稿</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.4.1 必要信息范围</h4> |
||||
|
<p>当您在登录状态下使用录音转文字功能,并对以及转写文稿进行云端存储、编辑、管理时,我们将收集您的录音以及编辑后的转写文稿信息,如文本信息、语音信息,同时需获取您的网络状态,在您允许我们开启存储与网络权限后,我们可以获取到您的录音以及编辑后的转写文稿信息 |
||||
|
,这是为了方便您随时导出、存储、分享您的录音以及编辑后的转写文稿。此类信息属于云端存储、编辑与管理录音及转写文稿必须的信息,如您拒绝提供,我们将无法为您提供云端存储、编辑、管理、分享、下载等服务,但不影响您使用本软件的其他功能。 |
||||
|
</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.4.2 调用的权限范围</h4> |
||||
|
<p>在您修改录音文件时,我们需要调用您的存储权限以存储您的文本信息 ,此为该功能服务所必须的系统权限,若您拒绝,您修改后的内容将不会存储至本地端。</p> |
||||
|
<h3>1.5 语音翻译</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.5.1 必要信息、非必要信息范围</h4> |
||||
|
<p>若您使用语音翻译服务,我们会申请您设备系统的麦克风权限、存储权限 。在经您授权同意后,我们会收集您的语音信息 |
||||
|
,以便/以用于获取说出的语音来进行翻译。此类信息属于语音转文字功能的必要信息,如您不能提供,您将无法使用语音翻译功能。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.5.2 调用的权限范围</h4> |
||||
|
<p>在您使用语音翻译和同声传译功能服务时我们需要调用您的麦克风权限以获取您的语音信息 ,此为该功能服务所必须的系统权限,若您拒绝,您将无法使用语音翻译和同声传译功能。</p> |
||||
|
<p>同时,我们需要调用您的存储权限以存储您的语音信息和文本信息,此为该功能服务所必须的系统权限,若您拒绝,您将无法使用语音翻译功能。</p> |
||||
|
<h3>1.7 设备权限</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.1 位置权限</h4> |
||||
|
<p>在使用智能翻译功能时,我们将收集您的定位信息以获得您的精确地理位置信息,用于实现智能翻译中的自动检测源语言功能,我们将根据您的定位信息自动匹配出您使用的语言,向您提供智能翻译服务。精确地理位置信息是个人敏感信息,若您拒绝开启GPS权限,我们将无法为您自动匹配翻译语言,需要您手动进行语言的选择。 |
||||
|
</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.2 蓝牙权限</h4> |
||||
|
<p>在您连接智能硬件时,我们需要获取您的蓝牙权限来搜索设备,传输配网信息,实现绑定设备目的。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.3 通讯录权限</h4> |
||||
|
<p>在您使用拨打电话功能时,我们需要获取您的通讯录权限获取信息,实现您给对应用户拨打电话的需求。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.4 短信权限</h4> |
||||
|
<p>在您使用智能助手时,我们需要获取您的短信权限,实现您给对应用户发送短信的需求。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.5 系统通知权限</h4> |
||||
|
<p>在向您推送 PUSH 消息时,我们需要获取您的通知权限。这是为了能在您的设备通知栏显示 APP 相关的通知内容,包括但不限于 APP |
||||
|
的新功能上线通知、您关注内容的更新信息、优惠活动提醒等。若您拒绝开启通知权限,您将无法及时收到这些重要信息,可能会错过 APP 为您准备的精彩内容和便利服务。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.7 存储权限(较少情况)</h4> |
||||
|
<p>在某些特殊的 PUSH |
||||
|
消息场景下,若消息需要将一些临时数据存储在本地设备上,我们需要获取您的存储权限。比如推送包含新功能配置文件的消息,需要存储权限来保存这些文件到本地,以便实现新功能在您设备上的应用。若您拒绝此权限,相关特殊功能可能无法正常使用。 |
||||
|
</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.8 相机权限</h4> |
||||
|
<p>当您使用设置头像或意见反馈上传图片功能时,我们会申请相机权限。在您明确授权后,相机将被用于拍摄照片以满足对应功能需求,且仅在相关功能运行期间收集和使用相机所产生的数据,功能结束后不再进行数据收集。若您拒绝授权相机权限,将无法使用涉及相机的相关功能,但不影响其他功能的正常使用。 |
||||
|
</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.8.1 相机权限的使用场景</h4> |
||||
|
<p>在Voitrans App中,我们有两个主要功能,需要使用到您的相机权限:</p> |
||||
|
<p>更改个人头像:当您希望更改您的个人头像时,我们需要调用相机权限以便您能够拍摄新的照片或从相册中选择一张图片作为头像。</p> |
||||
|
<p>意见反馈功能:在意见反馈功能中,如果您希望通过上传图片来提供更详细的反馈信息,我们需要相机权限来允许您拍摄照片或从相册中选择图片上传。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.8.2 用户控制</h4> |
||||
|
<p>您可以随时在设备的设置中开启或关闭相机权限。如果您选择关闭相机权限,您将无法使用上述功能,但不影响您使用Voitrans App的其他功能。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.8.3 数据的存储和处理</h4> |
||||
|
<p>照片存储:当您使用相机权限拍摄照片或选择图片时,这些图片仅在您的设备上存储,除非您主动选择上传。</p> |
||||
|
<p>数据处理:如果图片被上传到我们的服务器,我们将按照隐私政策中的规定处理这些数据,包括但不限于存储、备份和删除机制。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.8.4 数据的共享和转让</h4> |
||||
|
<p>我们不会与任何第三方共享或转让通过相机权限收集的图片数据,除非是为了提供服务所必需,或者您明确同意。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.8.5 用户撤销权限后的影响</h4> |
||||
|
<p>如果您撤销相机权限,您将无法使用更改个人头像和意见反馈中上传图片的功能。如果您希望重新授权,可以在设备的设置中重新开启相机权限。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.7.8.6 相机权限的安全性</h4> |
||||
|
<p>我们承诺采取行业标准的安全措施来保护您通过相机权限提供的数据,包括但不限于数据加密传输和安全存储。</p> |
||||
|
<h3>1.8 用户反馈与服务</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.8.1客服及问题</h4> |
||||
|
<p>为便于与您联系、尽快帮助您解决问题或记录相关问题的处理方案及结果,我们可能会保存您与我们的通信/通话记录及相关内容(包括账号信息、反馈信息、预约信息、您为了证明相关事实提供的其他信息,或您留下的联系方式信息),如果您针对具体预约进行咨询、投诉或提供建议的,我们会使用您的账号信息和预约信息。 |
||||
|
</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.8.2 投诉</h4> |
||||
|
<p>当您投诉他人或被他人投诉时,为了保护您及他人的合法权益,我们可能会将您的账号/身份信息、联系方式、投诉相关内容提供给消费者权益保护部门及监管机关,以便及时解决投诉纠纷,但法律法规明确禁止提供的除外。</p> |
||||
|
<p>如您通过我们产品或服务使用良闯时代关联方、合作方提供的服务并进行投诉时,为便于服务提供方(如良闯时代关联方、合作方)能与您有效联系并及时处理您的投诉,您同意我们获取您投诉时所提供的个人信息,并同步给对应服务提供方。</p> |
||||
|
<h3>1.9 运营与安全运行/保障</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.9.1 运营与安全保障</h4> |
||||
|
<p>我们致力于为您提供安全、可信的产品与使用环境,提供优质而可靠的服务与信息是我们的核心目标。为了维护Voitrans 平台的正常运行,保护您或其他用户或公众的合法利益免受损失,我们会收集用于维护产品或服务安全稳定运行的必要信息。 |
||||
|
</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">1.9.2 设备信息与日志信息</h4> |
||||
|
<p>a. 为了保障软件与服务的安全运行、运营的质量及效率,我们会收集您的硬件型号、操作系统版本号、设备标识符(Android如 IMEl 、 Android_ID 、 OAID 、 IMSI 、 ICCID 、 GAID 、 |
||||
|
MEID 、硬件序列号, iOS 如 IDFV 、 IDFA: 不同的标识符在有效期,是否可由用户重置以及获取方式方面会有所不同) 、网络设备硬件地址 (MAC 地址 ) 、 IP 地址、 WLAN 接入点 ( 如 SSID |
||||
|
, BSSID) 、蓝牙 (Bluetooth) 、基站、软件版本号、网络接入方式、类型、状态、网络质量数据、操作、使用、服务日志、设备传感器数据。</p> |
||||
|
<p>b. 为了预防恶意程序、保障运营质量及效率,我们会收集安装的应用信息或正在运行的进程信息、应用程序的总体运行、使用情况与频率。应用崩溃情况、总体安装使用情况、性能数据、应用来源。</p> |
||||
|
<p>c. 为了分析应用新增、激活、留存、性能等统计性指标,我们可能会调用剪切板对相关统计信息进行归因分析,请您放心,我们不会收集您的隐私信息。</p> |
||||
|
<p>d. 我们可能使用您的账户信息、设备信息、服务日志信息以及我们关联方、合作方在获得您授权或依法可以提供的信息,用于判断账户安全、进行身份验证、识别违法违规情况、检测及防范安全事件,并依法采取必要的记录、分析、处置措施。</p> |
||||
|
<p>1.10 收集、使用个人信息目的变更</p> |
||||
|
<p>请您了解,随着我们业务的发展,可能会对 Voitrans |
||||
|
的功能和提供的服务有所调整变化。原则上,当新功能或服务与我们当前提供的功能或服务相关时,收集与使用的个人信息将与原处理目的具有直接或合理关联。在与原处理目的无直接或合理关联的场景下,我们收集、使用您的个人信息,会再次按照法律法规及国家标准的要求以页面提示、交互流程、协议确认方式另行向您进行告知说明,并征得您的同意。 |
||||
|
</p> |
||||
|
<h3>1.11 征得授权同意的例外</h3> |
||||
|
<p>请您理解,在下列情形中,根据法律法规及相关国家标准,我们收集和使用您的个人信息不必事先征得您的授权同意:</p> |
||||
|
<p>a. 与我们履行法律法规规定的义务相关的;</p> |
||||
|
<p>b. 与国家安全、国防安全直接相关的;</p> |
||||
|
<p>c. 与公共安全、公共卫生、重大公共利益直接相关的;</p> |
||||
|
<p>d. 与刑事侦查、起诉、审判和判决执行等直接相关的;</p> |
||||
|
<p>e. 出于维护您或他人的生命、财产等重大合法权益但又很难得到本人授权同意的;</p> |
||||
|
<p>f. 您自行向社会公众公开的个人信息;</p> |
||||
|
<p>g. 根据个人信息主体要求签订和履行合同所必需的;</p> |
||||
|
<p>h. 从合法公开披露的信息中收集的您的个人信息的,如合法的新闻报道、政府信息公开等渠道;</p> |
||||
|
<p>i. 用于维护软件及相关服务的安全稳定运行所必需的,例如发现、处置软件及相关服务的故障;</p> |
||||
|
<p>j. 为开展合法的新闻报道所必需的;</p> |
||||
|
<p>k. 为学术研究机构,基于公共利益开展统计或学术研究所必要,且对外提供学术研究或描述的结果时,对结果中所包含的个人信息进行去标识化处理的;</p> |
||||
|
<p>l. 法律法规规定的其他情形。</p> |
||||
|
<p>特别提示您注意,如信息无法单独或结合其他信息识别到您的个人身份,其不属于法律意义上您的个人信息 ; |
||||
|
当您的信息可以单独或结合其他信息识别到您的个人身份时或我们将无法与任何特定个人信息建立联系的数据与其他您的个人信息结合使用时,这些信息在结合使用期间,将作为您的个人信息按照本隐私政策处理与保护。</p> |
||||
|
<h3>1.12 人机交互对话</h3> |
||||
|
<p>在您使用我们的人机交互服务时(包括智能助手,等),我们会收集您主动输入的内容(包括文本、图片、音视频等),并基于这些信息为您提供相应的服务。当您使用语音输入时,我们将收集您的音频数据,进行语音转写,并基于转写后的文本生成对话内容,以提供更好的互动体验。 |
||||
|
</p> |
||||
|
<p>如果您输入的信息包含他人个人数据,请确保已获得他人的合法授权,避免不当泄露他人隐私。</p> |
||||
|
<p>为了更好地理解您的需求和语境,我们会对您提供的信息进行分析和处理,以便输出更具相关性的内容。在确保信息经过加密、安全处理且去标识化的前提下,我们可能会将所收集的数据和生成的回复用于模型训练,持续优化服务质量。</p> |
||||
|
<h3>1.13 数据埋点统计及上报</h3> |
||||
|
<p>我们在 APP 内设置了页面埋点,目的是为了统计 APP 的使用情况,以便更好地优化产品性能、修复漏洞以及提升您的使用体验。当您访问 APP |
||||
|
页面时,页面埋点会收集您的页面浏览记录、点击行为、停留时长等信息,但此类信息的收集频率遵循合理必要原则,通常情况下,同一页面在您单次访问期间仅收集一次,不会对您造成频繁的数据收集干扰。仅在您进行交互操作(如点击按钮、切换页面板块等)触发新的业务需求时,才会根据实际需要再次收集与之相关的少量信息。 |
||||
|
</p> |
||||
|
<p>a. 我们收集的信息将主要用于为您提供、优化和改进本 APP 的服务,依据您的反馈修复程序问题等。</p> |
||||
|
<p>b. 部分信息可能会用于数据分析和研究,以了解用户群体的使用习惯和偏好趋势,从而推动本 APP 的持续创新发展。</p> |
||||
|
<h3>1.14 模型服务与数据处理</h3> |
||||
|
<p>1.14.1 模型服务基础</p> |
||||
|
<p>Voitrans 接入了目前国内比较优秀的的人工智能大模型服务,如豆包, DeepSeek,未来会持续优化模型的接入能力,拓展更多的模型,以为用户提供更好的服务。</p> |
||||
|
<p>1.14.2 数据收集与共享</p> |
||||
|
<p>在您使用第三方模型期间,为实现高效的聊天互动、回答问题、生成图像等服务,我们会收集您的输入内容并共享给相关服务提供方,以便根据您的内容提供文生文或文生图的人机交互服务。</p> |
||||
|
<p>1.14.3 模型服务说明</p> |
||||
|
<p>在您使用第三方模型期间,如遇到问题,可及时于我们取得联系,我们将持续优化模型的接入。</p> |
||||
|
<h3>1.15 应用自启动与关联启动说明</h3> |
||||
|
<p>1.15.1 自启动用途说明</p> |
||||
|
<p>Voitrans app 需要使用自启动能力以确保与外部蓝牙设备(如ble蓝牙耳机)保持稳定连接,这对于提供持续的语音交互和设备控制服务是必要的。自启动功能仅用于维持蓝牙连接服务,不会进行其他后台操作。</p> |
||||
|
<p>1.15.2 自启动场景说明</p> |
||||
|
<ul> |
||||
|
<li>蓝牙设备连接:当配对的蓝牙设备在范围内被激活时,应用需要自动启动以建立连接。</li> |
||||
|
<li>设备通信维护:为确保与蓝牙设备的实时通信质量,需要保持后台服务运行。</li> |
||||
|
<li>语音交互响应:当用户通过蓝牙设备发起语音指令时,应用需要及时响应。</li> |
||||
|
<li>设备状态监控:实时监控蓝牙设备的连接状态,确保服务的可靠性。</li> |
||||
|
</ul> |
||||
|
<h2>2. 数据使用过程中涉及的合作方以及转移、公开个人信息</h2> |
||||
|
<h3>2.1数据使用过程中涉及的合作方</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">2.1.1原则</h4> |
||||
|
<p>a.合法原则 :与合作方合作过程中涉及数据使用活动的,必须具有合法目的、符合法定的合法性基础。如果合作方使用信息不再符合合法原则,则其不应再使用您的个人信息,或在获得相应合法性基础后再行使用。</p> |
||||
|
<p>b.正当与最小必要原则 :数据使用必须具有正当目的,且应以达成目的必要为限。</p> |
||||
|
<p>c.安全审慎原则 :我们将审慎评估合作方使用数据的目的,对这些合作方的安全保障能力进行综合评估,并要求其遵循合作法律协议。我们会对合作方获取信息的软件工具开发包(SDK)、应用程序接口 (API) |
||||
|
进行严格的安全监测,以保护数据安全。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">2.1.2委托处理</h4> |
||||
|
<p>对于委托处理个人信息的场景,我们会与受托合作方根据法律规定签署相关处理协议,并对其个人信息使用活动进行监督。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">2.1.3共同处理</h4> |
||||
|
<p>对于共同处理个人信息的场景,我们会与合作方根据法律规定签署相关协议并约定各自的权利和义务,确保在使用相关个人信息的过程中遵守法律的相关规定、保护数据安全。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">2.1.4合作方的范围</h4> |
||||
|
<p>若具体功能和场景中涉及由我们的关联方、第三方提供服务,则合作方范围包括我们的关联方与第三方。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">2.1.5实现功能或服务的数据使用</h4> |
||||
|
<p>a.地理位置服务 : 当您使用地理位置相关服务时,位置服务提供商 ( 高德地图 ) 可能会通过 SDK 或相关技术使用您的 GPS |
||||
|
信息、设备信息以便可以向您返回位置结果。拒绝上述使用仅会影响地理位置服务功能,但不影响其他功能的正常使用。</p> |
||||
|
<p>b.客户服务 : 为及时处理您的投诉、建议以及其他诉求,客户服务提供商需要使用您的账号及所涉及事件的相关信息,以及时了解、处理和响应相关问题。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">2.1.6实现安全与分析统计的数据使用</h4> |
||||
|
<p>a.保障使用安全 : 我们非常重视账号、服务及内容安全,为保障您和其他用户的账号与财产安全,使您和我们的正当合法权益免受不法侵害,我们的合作方可能会使用必要的设备、账号及日志信息。</p> |
||||
|
<p>b.分析产品情况 : 为分析我们产品的使用和表现情况,我们的合作方可能需要使用该产品使用情况(崩溃、闪退 ) 、设备标识信息、应用总体安装使用情况等信息。</p> |
||||
|
<p>c. 为提升相关领域的科研能力,促进科技发展水平,我们在确保数据安全与目的正当的前提下,可能会与合作的科研院所、高校等机构使用去标识化或匿名化的数据。</p> |
||||
|
<h3>2.2 转移</h3> |
||||
|
<p>随着我们业务的持续发展,我们将有可能进行合并、收购、资产转让,您的个人信息有可能因此而被转移。在发生前述变更时,我们将按照法律法规及不低于本隐私政策所载明的安全标准要求继受方继续履行个人信息处理者的义务,若继受方变更原先的处理目的、处理方式的,我们将要求继受方重新征得您的授权同意。 |
||||
|
</p> |
||||
|
<h3>2.3 公开</h3> |
||||
|
<p>a. 我们不会公开您的信息,除非遵循国家法律法规规定或者获得您的同意。我们公开您的个人信息会采用符合行业内标准的安全保护措施。</p> |
||||
|
<p>b.对违规 账号 、欺诈行为进行处罚公告时,我们会披露相关账号的信息。</p> |
||||
|
<h3>2.4 依法豁免征得同意提供、公开的个人信息</h3> |
||||
|
<p>请您理解,在下列情形中,根据法律法规及国家标准,我们向合作方提供、公开您的个人信息无需征得您的授权同意 :</p> |
||||
|
<p>a. 为订立、履行您作为一方当事人的合同所必需,或者按照依法制定的劳动规章制度和依法签订的集体合同实施人力资源管理所必需;</p> |
||||
|
<p>b.为履行法定职责或者法定义务所必需 ;</p> |
||||
|
<p>c.与国家安全、国防安全直接相关的 ;</p> |
||||
|
<p>d.与刑事侦查、起诉、审判和判决执行等直接相关的 ;</p> |
||||
|
<p>e.为应对突发公共卫生事件,或者紧急情况下为保护自然人的生命健康和财产安全所必需 ;</p> |
||||
|
<p>f. 出于维护您或其他个人的生命、财产等重大合法权益但又很难得到您本人授权同意的;</p> |
||||
|
<p>g.为公共利益实施新闻报道、舆论监督等行为 , 在合理的范围内处理个人信息 ;</p> |
||||
|
<p>h.依照相关法律规定处理您自行公开或者其他已经合法公开的个人信息 ;</p> |
||||
|
<p>i.从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道;</p> |
||||
|
<p>j.法律、行政法规规定的其他情形。</p> |
||||
|
<h3>2.5 支付</h3> |
||||
|
<p>您授权我们通过指定的付款方式向您收取费用。当您使用Voitrans |
||||
|
app及相关服务涉及交易时,系统将会生成您的交易订单,您需要填写购买人姓名、订单相关手机号码,订单会自动生成您所购买的商品或服务信息、您应支付的货款金额及您选择的支付方式等支付信息,您可能在订单上补充告知相关方的其他信息以方便交易的实现。 |
||||
|
</p> |
||||
|
<p>当您使用涉及支付的功能时:支付通过合作的第三方支付机构(微信支付苹果支付等)所提供的支付服务完成,第三方支付服务有可能使用生物识别信息用于付款验证,您在使用第三方支付服务的时候请阅读该第三方机构的用户协议、隐私政策,如对于该第三方机构的用户协议、隐私政策不同意的,请您及时停止使用该第三方机构的支付服务。 |
||||
|
</p> |
||||
|
<h2>3. 我们如何存储个人信息</h2> |
||||
|
<h3>3.1存储地点</h3> |
||||
|
<p>我们依照法律法规的规定,将在境内运营过程中收集和产生的您的个人信息 存储于中华人民共和国境内。目前,我们不会将上述信息传输至境外,如果我们向境外传输,会严格遵守中国的相关法律、监管政策,并会遵循相关国家规定或者征求您的同意。 |
||||
|
</p> |
||||
|
<h3>3.2 存储期限</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">3.2.1 我们仅在为提供Voitrans 服务之目的所必需的期间内保留您的个人信息,例如:</h4> |
||||
|
<p>a. 手机号码: 当您使用手机号码注册登录时,我们需要持续保留您的手机号码,以便于向您提供正常的服务、应对您可能的查询和客诉并保障您的账号和系统安全。</p> |
||||
|
<p>b.电子邮箱:当您使用电子邮箱注册登录时,我们需要持续保留您的电子邮箱,以便于向您提供正常的服务、应对您可能的查询和客诉并保障您的账号和系统安全。</p> |
||||
|
<p>c.第三方授权:当您使用其他合作方授权方式登录时,我们会保留您的授权信息,以便您可以持续使用第三方登录方式进行登录,使得我们可以持续为您提供服务。</p> |
||||
|
<p>c. 您发布的信息、评论、点赞及相关信息,在您未撤回、删除或未注销账号期间,我们会保留相关信息用于向您提供持续的业务功能。</p> |
||||
|
<p>d. 根据《中华人民共和国网络安全法》、《中华人民共和国电子商务法》相关规定保留您的个人信息。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">3.2.2 |
||||
|
如果您注销账号、主动删除个人信息或超出必要的期限后,我们将对您的个人信息进行删除或匿名化处理,但以下情况除外:</h4> |
||||
|
<p>a .遵从法律法规有关信息留存的要求;</p> |
||||
|
<p>b .遵守法院判决、裁定或其他法律程序的规定;</p> |
||||
|
<p>c. 遵守相关政府机关或法定授权组织的要求;</p> |
||||
|
<p>d. 出于财务、审计、争议解决等目的需要合理延长期限的。</p> |
||||
|
<h2>4. 我们如何保护个人信息的安全</h2> |
||||
|
<h3>4.1 我们非常重视您个人信息的安全,将努力采取合理的安全措施 ( 包括技术方面和管理方面 ) 来保护您的个人信息,防止您提供的个人信息被不当使用或未经授权的情况下被访问、公开披露、使用、修改、损坏、丢失或泄漏。</h3> |
||||
|
<h3>4.2 我们会使用不低于行业同行的加密技术、 匿 名化处理及相关合理可行的手段保护您的个人信息,并使用安全保护机制防止您的个人信息遭到恶意攻击。</h3> |
||||
|
<h3>4.3 我们会建立专门的安全部门、安全管理制度、数据安全流程保障您的个人信息安全。我们采取严格的数据使用和访问制度,确保只有授权人员才可访问您的个人信息,并适时对数据和技术进行安全审计。</h3> |
||||
|
<h3>4.4 |
||||
|
尽管已经采取了上述合理有效措施,并已经遵守了相关法律规定要求的标准,但请您理解,由于技术的限制以及可能存在的各种恶意手段,在互联网行业,即便竭尽所能加强安全措施,也不可能始终保证信息百分之百的安全,我们将尽力确保您提供给我们的个人信息的安全性。 |
||||
|
</h3> |
||||
|
<h3>4.5 您知悉并理解,您接入我们的服务所用的系统和通讯网络,有可能因我们可控范围外的因素而出现问题。因此,我们强烈建议您采取积极措施保护个人信息的安全,包括但不限于使用复杂密码、定期修改密码、不将自己的 账号 |
||||
|
密码及相关个人信息透露给他人。</h3> |
||||
|
<h3>4.6 我们会制定应急处理预案,并在发生用户信息安全事件时立即启动应急预案,努力阻止这些安全事件的影响和后果扩大。一 旦 发生用户信息安全事件(泄露、丢失 ) 后,我们将按照法律法规的要求,及时向您告知 : |
||||
|
安全事件的基本情况和可能的影响,我们已经采取或将要采取的处置措施、您可自主防范和降低风险的建议、对您的补救措施。我们将及时将事件相关情况以推送通知、邮件、信函、短信及相关形式告知您,难以逐一告知时,我们会采取合理、有效的方式发布公告。同时,我们还将按照相关监管部门要求,上报用户信息安全事件的处置情况。 |
||||
|
</h3> |
||||
|
<h3>4.7 您一旦离开Voitrans 平台及相关服务,浏览或使用其他网站、服务及内容资源,我们将没有能力和直接义务保护您在Voitrans |
||||
|
平台及相关服务之外的软件、网站提交的任何个人信息,无论您登录、浏览或使用上述软件、网站是否基于Voitrans 平台的链接或引导。</h3> |
||||
|
<h2>5. 我们如何管理您的个人信息</h2> |
||||
|
<p>我们非常重视您对个人信息的管理,并尽全力保护您对于您个人信息的查阅、复制、更正、补充、删除、撤回同意授权、注销账号、投诉举报以及设置隐私功能等权利,以使您有能力保障您的隐私和信息安全。</p> |
||||
|
<h3>5.1 改变或撤回授权范围</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">5.1.1 改变或撤回敏感权限设置 : |
||||
|
特定的业务功能和服务将需要您的设备敏感权限才能得以完成,当您改变或撤回敏感权限后,我们无法继续为您提供撤回敏感权限所对应的功能和服务,也不再处理您相应的敏感权限信息。但您撤回敏感权限的决定,不会影响我们此前基于您的授权而开展的个人信息处理。您可以通过 |
||||
|
更改移动终端的设置 开启或关闭敏感权限。</h4> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">5.1.2 改变或撤回授权的信息处理 : |
||||
|
特定的业务功能和服务将需要您的信息才能得以完成,当您撤回同意或授权后,我们无法继续为您提供撤回同意或授权所对应的功能和服务,也不再处理您相应的个人信息。但您撤回同意或授权的决定,不会影响我们此前基于您的授权而开展的个人信息处理。 |
||||
|
</h4> |
||||
|
<h3>5.2 您的个人信息权利</h3> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">5.2.1 查阅个人信息 :</h4> |
||||
|
<p>您可以通过Voitrans APP【个人中心】查阅您的昵称、手机号等个人信息。</p> |
||||
|
<h4 style="font-size: 16px; margin-top: 12px; color: #555;">5.2.2 更正个人信息 :</h4> |
||||
|
<p>您可以通过Voitrans APP【个人中心】修改您的昵称 等个人信息 。</p> |
||||
|
<p>特别提示您注意,出于安全性和身份识别(如号码申诉服务 ) 的考虑,您可能无法自主修改注册时提交的某些初始注册信息。如您确有需要修改该类注册信息,请根据本隐私政策载明的联系方式联系我们。</p> |
||||
|
<p>除上述外,如果您或其他有权主体对相关法律法规赋予的个人信息权利的行使,有任何主张,要求或者疑问,您可以通过我们预留的联系方式(官网、APP内)与我们联系,我们将尽快审核所涉问题,并在验证您的用户身份后的1 0 个工作日内回复。 |
||||
|
</p> |
||||
|
<h3>5.3 注销 账号</h3> |
||||
|
<p>您可以通过Voitrans APP【设置】 -【个人中心】 - 【注销 账号 】与我们联系申请注销您的账号,我们将会在收到您的申请后及时进行处理,并在1 0 |
||||
|
个工作日或法律法规要求的时限内回复您的请求。在您注销账号前,我们将验证您的个人身份、安全状态、设备信息等。您知悉并理解,注销账号是不可逆的行为。</p> |
||||
|
<h3>5.4 意见反馈和投诉举报</h3> |
||||
|
<p>您可以按照我们公示的制度进行投诉或举报。如果您有任何意见或您认为您的个人信息权利可能受到侵害,或者发现侵害个人信息权利的线索,您可以通过Voitrans APP【设置】 - 【用户反馈】与我们联系。</p> |
||||
|
<h3>5.5 访问隐私政策</h3> |
||||
|
<p>您可以在Voitrans平台的注册页面,或者在Voitrans APP【设置】 -【个人中心】 - 【 隐私政策 】查看本隐私政策。</p> |
||||
|
<p>请您了解,本隐私政策中所述的 Voitrans及相关服务可能会根据您所使用的手机型号、系统版本、软件应用程序版本、移动客户端等因素而有所不同。最终的产品和服务以您所使用的Voitrans 软件及相关服务为准。</p> |
||||
|
<h2>6. 停止运营并向您告知</h2> |
||||
|
<p>如我们停止运营,我们将及时停止收集您个人信息的活动,将停止运营的通知以逐一送达或公告的形式通知您,并对所持有的您的个人信息进行删除或匿名化处理。</p> |
||||
|
<h3>6.1 未成年人通用条款</h3> |
||||
|
<p>a. 若您是已满14周岁但未满 18 周岁的未成年人,在使用Voitrans平台及相关服务前,应在您的父母或其他监护人监护、指导下共同阅读并同意本隐私政策。</p> |
||||
|
<p>b. 若您是未满 14 周岁的未成年人的监护人,在使用Voitrans 平台及相关服务前,应由您代被监护人阅读并同意本隐私政策。</p> |
||||
|
<p>c. 我们根据国家相关法律法规的规定保护未成年人的个人信息,只会在法律允许、父母或其他监护人明确同意或保护未成年人所必要的情况下收集、使用或披露未成年人的个人信息 ; |
||||
|
如果我们发现在未事先获得可证实的父母或其他监护人同意的情况下收集了未成年人的个人信息,则会设法尽快删除相关信息。同时我们建立了严格的儿童信息收集使用规则,以保护儿童个人信息安全。</p> |
||||
|
<p>d. 若您是未成年人的监护人,当您对您所监护的未成年人的个人信息有相关疑问时,请通过本隐私政策公示的联系方式与我们联系。</p> |
||||
|
<h2>7. 隐私政策的修订和通知</h2> |
||||
|
<h3>7.1 为了给您提供更好的服务,Voitrans |
||||
|
平台及相关服务将不时更新与变化,我们会适时对本隐私政策进行修订,这些修订构成本隐私政策的一部分并具有等同于本隐私政策的效力,未经您明确同意,我们不会削减您依据当前生效的本隐私政策所应享受的权利。</h3> |
||||
|
<h3>7.2 本隐私政策更新后,我们会在Voitrans 平台发出更新版本,并在更新后的条款生效前通过官方系统公告或其他适当的方式提醒您更新的内容,以便您及时了解本隐私政策的最新版本。</h3> |
||||
|
<h2>8. 联系我们</h2> |
||||
|
<h3>8.1 如果您对个人信息保护问题有投诉、建议、疑问,您可以通过Voitrans APP【设置】 - 【用户反馈】与我们联系,我们核查并验证您的用户身份后会及时给您反馈,一般情况下我们将在10个工作日内回复您的请求。</h3> |
||||
|
<h3>8.2 如对本隐私政策内容有任何疑问、意见或建议,您可以通过Voitrans APP【设置】 - 【用户反馈】与我们联系。</h3> |
||||
|
<h2>9. 第三方信息共享清单</h2> |
||||
|
<p>以下为我们接入的第三方SDK名称、开发者信息及其隐私政策链接说明,便于您更清楚地了解您的个人信息使用与保护情况:</p> |
||||
|
<h3>9.1 微信OpenSDK Android</h3> |
||||
|
<p>信息获取:设备号、IP地址、设备软件信息、设备识别码(IMEI、Android ID、IDFA、OAID、OPENUDID、GUID、ICCID、SIM 卡 IMSI |
||||
|
信息、IP、MAC地址)、设备识别符、所在地区、设备相关应用信息以及其他与服务相关的日志信息</p> |
||||
|
<p>使用目的:微信登录与分享,详情见SDK隐私政策</p> |
||||
|
<p>使用场景:在用户使用微信登录与分享时使用,详情见SDK隐私政策</p> |
||||
|
<p>合作方主体:深圳市腾讯计算机系统有限公司</p> |
||||
|
<p>收集方式:SDK自行采集</p> |
||||
|
<p>隐私政策链接:https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/RYiYJkLOrQwu0nb8</p> |
||||
|
<h3>9.2 导航SDK</h3> |
||||
|
<p>信息获取:应用名、应用版本号、设备品牌及型号、操作系统、运营商信息、屏幕分辨率、传感器信息(矢量、加速度、压力、方向、地磁 、陀螺、温度、光敏、nema信号)、设备信号强度信息</p> |
||||
|
<p>使用目的:使用高德地图进行导航</p> |
||||
|
<p>使用场景:使用高德地图进行导航</p> |
||||
|
<p>合作方主体:北京高德图强科技有限公司</p> |
||||
|
<p>收集方式:SDK自行采集</p> |
||||
|
<p>隐私政策链接:https://lbs.amap.com/pages/privacy/</p> |
||||
|
<h3>9.3 地图SDK</h3> |
||||
|
<p>信息获取:应用名、应用版本号、设备品牌及型号、操作系统、网络信息、设备信号强度信息等</p> |
||||
|
<p>使用目的:地图展示与交互</p> |
||||
|
<p>使用场景:地图相关功能</p> |
||||
|
<p>合作方主体:北京高德图强科技有限公司</p> |
||||
|
<p>收集方式:SDK自行采集</p> |
||||
|
<p>隐私政策链接:https://lbs.amap.com/pages/privacy/</p> |
||||
|
<h3>9.4 定位SDK</h3> |
||||
|
<p>信息获取:位置信息(GPS、Wi-Fi、基站)、设备信息、网络信息等</p> |
||||
|
<p>使用目的:提供定位服务</p> |
||||
|
<p>使用场景:位置相关功能</p> |
||||
|
<p>合作方主体:北京高德图强科技有限公司</p> |
||||
|
<p>收集方式:SDK自行采集</p> |
||||
|
<p>隐私政策链接:https://lbs.amap.com/pages/privacy/</p> |
||||
|
<h3>9.5 轻量版地图SDK</h3> |
||||
|
<p>信息获取:应用名、应用版本号、设备信息、网络信息等</p> |
||||
|
<p>使用目的:地图工具与渲染优化</p> |
||||
|
<p>使用场景:地图相关工具功能</p> |
||||
|
<p>合作方主体:北京高德图强科技有限公司</p> |
||||
|
<p>收集方式:SDK自行采集</p> |
||||
|
<p>隐私政策链接:https://lbs.amap.com/pages/privacy/</p> |
||||
|
<h3>9.6 APP支付客户端SDK</h3> |
||||
|
<p>信息获取:设备标识、设备信息、软件安装列表、网络信息等(详见官方隐私政策)</p> |
||||
|
<p>使用目的:支付宝账号登录与调起支付能力</p> |
||||
|
<p>使用场景:支付相关功能</p> |
||||
|
<p>合作方主体:支付宝(杭州)信息技术有限公司</p> |
||||
|
<p>收集方式:SDK自行采集</p> |
||||
|
<p>隐私政策链接:https://opendocs.alipay.com/open/54/01g6qm#%E6%94%AF%E4%BB%98%E5%AE%9D%20App%20%E6%94%AF%E4%BB%98%E5%AE%A2%E6%88%B7%E7%AB%AF%20SDK%20%E9%9A%90%E7%A7%81%E6%94%BF%E7%AD%96</p> |
||||
|
<h3>9.7 灯塔 SDK Android</h3> |
||||
|
<p>信息获取:SDK或API版本、应用标识符、应用版本、终端厂商、操作系统版本、语言、时区、网络状态、崩溃与使用日志、IP地址等(详见官方隐私政策)</p> |
||||
|
<p>使用目的:应用质量与数据上报服务</p> |
||||
|
<p>使用场景:稳定性监测与性能分析</p> |
||||
|
<p>合作方主体:深圳市腾讯计算机系统有限公司</p> |
||||
|
<p>收集方式:SDK自行采集</p> |
||||
|
<p>隐私政策链接:https://privacy.qq.com/document/preview/a19a8cf4f2354306b137a7db39d32024</p> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
||||
@ -0,0 +1,381 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>Voitrans Privacy Policy</title> |
||||
|
<style> |
||||
|
body { |
||||
|
font-family: "Calibri", "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; |
||||
|
line-height: 1.6; |
||||
|
color: #333; |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
background-color: #fff; |
||||
|
overflow-x: hidden; |
||||
|
word-wrap: break-word; |
||||
|
word-break: break-all; |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
max-width: 800px; |
||||
|
margin: 0 auto; |
||||
|
padding: 20px 40px; |
||||
|
} |
||||
|
|
||||
|
h1 { |
||||
|
font-size: 24px; |
||||
|
font-weight: bold; |
||||
|
text-align: center; |
||||
|
margin-bottom: 24px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 20px; |
||||
|
font-weight: bold; |
||||
|
margin-top: 24px; |
||||
|
margin-bottom: 12px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
font-size: 18px; |
||||
|
font-weight: bold; |
||||
|
margin-top: 20px; |
||||
|
margin-bottom: 10px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
h4 { |
||||
|
font-size: 16px; |
||||
|
font-weight: bold; |
||||
|
margin-top: 16px; |
||||
|
margin-bottom: 8px; |
||||
|
color: #555; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
margin-bottom: 12px; |
||||
|
text-align: justify; |
||||
|
font-size: 14px; |
||||
|
word-wrap: break-word; |
||||
|
word-break: break-all; |
||||
|
} |
||||
|
|
||||
|
ul, ol { |
||||
|
margin-bottom: 12px; |
||||
|
padding-left: 20px; |
||||
|
} |
||||
|
|
||||
|
li { |
||||
|
margin-bottom: 6px; |
||||
|
font-size: 14px; |
||||
|
word-wrap: break-word; |
||||
|
word-break: break-all; |
||||
|
} |
||||
|
|
||||
|
.date-info { |
||||
|
font-size: 14px; |
||||
|
color: #666; |
||||
|
margin-bottom: 5px; |
||||
|
} |
||||
|
|
||||
|
/* Responsive adjustments */ |
||||
|
@media (max-width: 600px) { |
||||
|
.container { |
||||
|
padding: 15px; |
||||
|
} |
||||
|
|
||||
|
h1 { |
||||
|
font-size: 22px; |
||||
|
} |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 18px; |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<div class="container"> |
||||
|
<h1>Voitrans Privacy Policy</h1> |
||||
|
<p class="date-info">Last Updated: May 27, 2025</p> |
||||
|
<p class="date-info">Effective Date: May 27, 2025</p> |
||||
|
|
||||
|
<p>Welcome to use Voitrans Software and Services</p> |
||||
|
<p>1. Voitrans Provided by Saitong Industries Inc. (hereinafter referred to as "the Company"). Can help users achieve Bluetooth devices and other products management. We understand the importance of personal information to our users (hereinafter referred to as "you") and that effective protection of your personal information is the cornerstone of the healthy and sustainable development of our business. Thank you for your use and trust in Voitrans products or services! We are fully aware that this trust does not come easily, we adhere to the principle of openness and transparency, to express to you our rules for processing your personal information, we use legal and legitimate ways and clear and reasonable purposes to process your personal information. Special note: Please be sure to read and understand this Privacy Policy carefully before using our products or services so that you can make appropriate choices.</p> |
||||
|
<p>2. This Privacy Policy explains to you how we collect, use, store and share your personal information when you use Voitrans. Personal information refers to all kinds of information recorded by electronic or other means that can identify a specific natural person or reflect the activities of a specific natural person alone or in combination with other information. Voitrans respects and protects the personal privacy of all users of the service. Except as otherwise provided in this Privacy Policy, Voitrans will not disclose this information to the public or provide it to third parties without your prior permission.</p> |
||||
|
<p>3. When you use a third party's product or service through an App Marketplace product or service, your personal information shall be subject to the third party's privacy policy.</p> |
||||
|
<p>4. Voitrans This Privacy Policy will be updated from time to time. By agreeing to the Voitrans Service Usage Agreement and beginning to use Voitrans, you are deemed to have agreed to all of this Privacy Policy. If you do not agree to this Privacy Policy, please do not use Voitrans Software and Services.</p> |
||||
|
<p>5. In order to analyze and measure the effectiveness of relevant services, we will use and disclose your personal information in accordance with the provisions of this Privacy Policy.</p> |
||||
|
|
||||
|
<h3>This Privacy policy will help you understand the following:</h3> |
||||
|
<ul> |
||||
|
<li>1 The information we collect</li> |
||||
|
<li>2 Storage of information</li> |
||||
|
<li>3 Information security</li> |
||||
|
<li>4 How do we use the information we collect</li> |
||||
|
<li>5 Information sharing</li> |
||||
|
<li>6 How do you manage your personal information</li> |
||||
|
<li>7 Application permission description</li> |
||||
|
<li>8 Access third-party SDKS</li> |
||||
|
<li>9 Protection of minors</li> |
||||
|
<li>10 Effective and updated Privacy Policy</li> |
||||
|
<li>11 How to contact us</li> |
||||
|
</ul> |
||||
|
|
||||
|
<h3>1. The information we collect</h3> |
||||
|
<p>1. Use Voitrans at the beginning We will start collecting data about your usage of the application</p> |
||||
|
<p>2. Voitrans may obtain personal information about you from third parties or business partners through lawful means</p> |
||||
|
<p>3. In order to provide you with better products and services, give you network security during use, maintain the normal operation of products and optimize and improve, the purpose of our information services, after you actively click to agree to this privacy policy, use the information we provide. During the service, in addition to the personal information necessary to ensure the normal operation of the basic functions of the App in accordance with national laws and regulations, we will collect other personal information from you:</p> |
||||
|
<p>3.1 In order to identify risk users, judge the authenticity of users and prevent cheating in an all-round way, we will read your device information every time you open the software, including: (Device model, device MAC address, device unique identifier (IMEI, AndroidID, Device ID, MEID, UUID, GAID, OAID, IDFA, OpenUDID, GUID, IMSI, CCID, etc.) MEID, serial number, SSID and BSSID of WIFI), read Bluetooth address, operating system version, operating system name, user login account and password information;</p> |
||||
|
<p>3.2 When you use our products and services, we may collect certain information through network log information or certain programs and software, such information may include: details of the use of products and services, such as the opening time of your application, the length of use, and the address of the page visited; Device event information, such as crashes, system activity, etc., to optimize your device and improve our services;</p> |
||||
|
<p>3.3 To identify applications where equipment is at risk, provide more secure functional services. Therefore, we need to obtain your application/application installation list in the process to ensure the normal operation of the product and protect the security of the device.</p> |
||||
|
<p>3.4 In order to analyze the use of application functions by users and determine the authenticity of users, we need to conduct data statistics, and collect the above device information, including device MAC address and device unique identifier, when your device is in use.</p> |
||||
|
<p>3.5 Device signal: the network status of the device you use, WIFI access point information, etc.</p> |
||||
|
<p>3.6 Location Information: We may use GPS, WiFi or other technical means to collect and process information about your location for updating local information, providing content services, etc.</p> |
||||
|
<p>For more information, please click "Personal Information Collection List".</p> |
||||
|
|
||||
|
<p>1. For the more sensitive personal information in the above information, such as user device information (IMEI number, call status and identification number), GPS location information, photo media content and file access rights, we will obtain your consent before collection. For some non-sensitive information, the collection may be default and automatic. You can restrict our collection of relevant information or use of certain permissions from your mobile device by adjusting the Settings option for your mobile device. When you restrict our access to a certain permission, we may not be able to continue to provide you with the corresponding services, and no longer process your corresponding information.</p> |
||||
|
<p>2. We will not collect your ID card number, personal biometric information, bank account number, property information, credit information, health and physiological information and other personal sensitive information.</p> |
||||
|
<p>3. About sensitive permissions: In order to properly use the application functions, the product needs to obtain the following permissions in order to provide functional services related to normal products.</p> |
||||
|
|
||||
|
<h4>1 Obtain storage read and write permissions</h4> |
||||
|
<p>1 How to get: pop-up prompt, when the permission is obtained, the user will take the initiative to click agree application will use this permission</p> |
||||
|
<p>2 Purpose and scope of use: It is used to scan the resource cache files and thumbnails in the system, load music resources and product pictures. It is displayed on the App interface to facilitate users to view and operate</p> |
||||
|
|
||||
|
<h4>2 Obtain permission to the application installation list</h4> |
||||
|
<p>1 How to get: pop-up prompt, when the permission is obtained, the user will take the initiative to click agree application will use this permission</p> |
||||
|
<p>2 Purpose and scope of use: When the application needs to use advertising promotion services, the third-party advertising platform, Youmon +SDK, needs to obtain the "app software installation list permission" of the device to prevent the user from pushing the application already owned by the user and improve the user experience.</p> |
||||
|
|
||||
|
<h4>3 The permission to obtain device status information</h4> |
||||
|
<p>Includes: (Device model, device MAC address, device unique identifier (IMEI, AndroidID, Device ID, MEID, UUID, GAID, OAID, IDFA, OpenUDID, GUID, IMSI, CCID, etc.) MEID, serial number, SSID and BSSID of WIFI, read Bluetooth address), operating system version, operating system name, etc.;</p> |
||||
|
<p>1 How to get: pop-up prompt, when the permission is obtained, the user will take the initiative to click agree application will use this permission</p> |
||||
|
<p>2 Purpose and Scope of use: When using our application, in order to analyze the user's use of application functions and judge the user's authenticity, we will synchronize the permission to third-party SDKS, including Autonavi SDK, Youmon +SDK, etc.</p> |
||||
|
|
||||
|
<h4>4 Access to location information</h4> |
||||
|
<p>1 How to obtain: pop-up prompts, when the permission is obtained, the user will use this permission only when he actively clicks agree with the application.</p> |
||||
|
<p>2 Purpose and Scope of use: When the application needs to use GPS, WiFi or other functions, the product needs to collect your location and information to search for Bluetooth devices in nearby locations.</p> |
||||
|
<p>Note: For more application permissions, see this document "Privacy Policy" Section 7 "Application Permission Description"</p> |
||||
|
|
||||
|
<h3>2. Storage of information</h3> |
||||
|
<p>1. Where the information is stored</p> |
||||
|
<p>The personal information we collect and generate in the People's Republic of China will be stored on our servers and/or those of our affiliates.</p> |
||||
|
<p>2. Information storage and processing</p> |
||||
|
<p>We store your personal information only for the period necessary to achieve the purpose or as long as required by law and regulation. After you stop using this product, we will immediately stop collecting and processing your personal information and application data. When the above period is exceeded, we will delete or anonymize the personal information you have provided to us.</p> |
||||
|
<p>In addition to the necessary functions, Voitrans needs to apply for corresponding system permissions. The server side of the cloud does not store user personal information, and the third-party SDK accessed will be anonymized and de-identified by the service provider (Amu Statistics SDK, Autonavi SDK), so users cannot be tracked. Information stored on the user's phone will be automatically deleted when the user uninstalls Voitrans or clears the cache.</p> |
||||
|
|
||||
|
<h3>3. Information security</h3> |
||||
|
<p>1. Voitrans stores and protects your personal information using industry-standard security measures and technologies, such as authentication, data encryption, restricted data center access and other security measures to ensure that your information is secure from disclosure, alteration and destruction. Despite the aforementioned security measures, please note that no security measure can be 100% secure.</p> |
||||
|
<p>2. When you use the services provided by Voitrans, please protect your personal information. If you find that your personal information has been compromised, please contact us immediately so that we can take appropriate measures.</p> |
||||
|
|
||||
|
<h3>4. How do we use the information we collect</h3> |
||||
|
<p>1. When implementing application business functions, we may use your personal information to ensure the integrity and security of the products and services we provide to you;</p> |
||||
|
<p>2. We will understand your usage habits through the collected information, and with the help of data analysis, identify the acceptability of services, your usage habits, etc., in order to test and create new products, functions and services, improve the service level, and help us improve existing services and develop new services;</p> |
||||
|
<p>3. We may use your personal information for customer surveys about the products and services, for sending notices about the products and services, and for commercial promotion;</p> |
||||
|
<p>4. We may use your information for internal analysis and research related to the application;</p> |
||||
|
|
||||
|
<h3>5. Information sharing</h3> |
||||
|
<p>At present, we will not actively share or transfer your personal information to third parties outside the company, if there is other sharing or transfer of your personal information, we will directly or confirm that the third party has obtained your express consent to the above behavior.</p> |
||||
|
<p>We will not publicly disclose the personal information we collect. If it is necessary to publicly disclose it, we will inform you of the purpose of the public disclosure, the type of information disclosed and the sensitive information that may be involved, and obtain your express consent.</p> |
||||
|
<p>As our business continues to develop, we may engage in mergers, acquisitions, asset transfers and other transactions, and we will inform you of the relevant circumstances, in accordance with laws and regulations and no less than the standards required by this Guide to continue to protect your personal information or require a new controller to continue to protect your personal information.</p> |
||||
|
<p>In addition, according to relevant laws and regulations and national standards, we may share, transfer and publicly disclose personal information without prior authorization and consent of the personal information subject under the following circumstances:</p> |
||||
|
<ul> |
||||
|
<li>1 Directly related to national security and national defense security;</li> |
||||
|
<li>2 Directly related to public safety, public health or major public interests;</li> |
||||
|
<li>3 Directly related to criminal investigation, prosecution, trial and execution of judgments;</li> |
||||
|
<li>4 In order to protect the life, property and other major legitimate rights and interests of the personal data subject or other individuals, but it is difficult to obtain the consent of the person;</li> |
||||
|
<li>5 Personal information disclosed to the public by the personal information subject;</li> |
||||
|
<li>6 Collecting personal information from legally publicly disclosed information, such as legitimate news reports, government information disclosure and other channels.</li> |
||||
|
</ul> |
||||
|
|
||||
|
<h3>6. How do you manage your personal information</h3> |
||||
|
<p>In accordance with the relevant laws, regulations and standards of China, as well as the common practices of other countries and regions, we guarantee that you exercise the following rights to your personal information:</p> |
||||
|
|
||||
|
<h4>1. Correct your personal information</h4> |
||||
|
<p>If you find that there is an error in your personal information processed by us, you have the right to correct or update the wrong or incomplete information. If you are unable to operate independently in the account center, you can email saitong.voitrans@gmail.com Contact us</p> |
||||
|
|
||||
|
<h4>2. Delete your personal information</h4> |
||||
|
<p>In the following cases, you may make a request to us to delete your personal information:</p> |
||||
|
<ul> |
||||
|
<li>If we process personal information in violation of laws and regulations;</li> |
||||
|
<li>If we collect and use your personal information without your consent;</li> |
||||
|
<li>If our processing of personal information violates our agreement with you;</li> |
||||
|
<li>If you no longer use our products or services;</li> |
||||
|
<li>If we no longer provide you with products or services.</li> |
||||
|
</ul> |
||||
|
<p>Contact us. We will process your request within saitong.voitrans@gmail.com Contact us. We will send it to you after receiving it 30 working days of receipt</p> |
||||
|
|
||||
|
<h4>3. Change the scope of your consent</h4> |
||||
|
<p>You can contact us by email saitong.voitrans@gmail.com Contact us. At your request, we will no longer collect, use or disclose your personal information. Please note that your withdrawal of consent or deletion of your personal information may affect your use of some of our services.</p> |
||||
|
|
||||
|
<h4>4 Access and copy personal information</h4> |
||||
|
<p>You may, in accordance with the provisions of the relevant laws, provide your imei (phone serial number) or oaid Or Android ID After the account information and other information, you can apply to us for access to and copy your personal information, you can email to 3514406195@qq.com Get in touch with us.</p> |
||||
|
|
||||
|
<h4>5. In response to your above request</h4> |
||||
|
<p>For security purposes, you may need to provide a written request or otherwise prove your identity. We may ask you to verify your identity before processing your request. For your reasonable request, we will respond and deal with it in a timely manner. We may reject requests that are unnecessarily repetitive, require too much technology (for example, the development of new systems or fundamental changes to existing practices), pose a risk to the legitimate interests of others, or are highly impractical.</p> |
||||
|
<p>In the following circumstances, according to the requirements of laws and regulations, we will not be able to respond to your request:</p> |
||||
|
<ul> |
||||
|
<li>Related to national security and national defense security;</li> |
||||
|
<li>In connection with criminal investigation, prosecution and trial;</li> |
||||
|
<li>There is sufficient evidence that you have subjective malice or abuse of rights;</li> |
||||
|
<li>Responding to your request will cause serious damage to the legitimate rights and interests of you or other individuals and organizations.</li> |
||||
|
</ul> |
||||
|
|
||||
|
<h4>6. Account cancellation</h4> |
||||
|
<p>User can be in Perform [account cancellation] operation on the APP. Operation steps:</p> |
||||
|
<ol> |
||||
|
<li>Go to the [My] page</li> |
||||
|
<li>Click the [Settings] button on the upper right</li> |
||||
|
<li>Click [Cancel account] to confirm</li> |
||||
|
</ol> |
||||
|
<p>After you log out, the information about the user account is cleared</p> |
||||
|
|
||||
|
<h3>7. Application permission description</h3> |
||||
|
<p>In order to provide better products and services, when you use the software, we will apply for the following rights, when you enter the home page or use the function will request authorization, if you refuse, some of the corresponding functions will not be able to use normally. The specific purposes and reminders of permissions are described in detail as follows:</p> |
||||
|
|
||||
|
<h4>1 Location rights:</h4> |
||||
|
<p>android.permission.ACCESS_COARSE_LOCATION</p> |
||||
|
<p>android.permission.ACCESS_FINE_LOCATION</p> |
||||
|
<p>pass WiFi or mobile base station to obtain the user's latitude and longitude location information</p> |
||||
|
<p>When you use In Voitrans, we will search for your attached Bluetooth device based on your geographical location. At the same time, in order to provide more stable services, optimize products and improve experience, we will share the above information with three SDKS, such as Youmon and Autonavi, to obtain Voitrans operating data and functional services.</p> |
||||
|
<p>Reminder mode: A prompt indicating the permission to start the application for the first time is displayed, requiring the user to confirm acceptance. User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<h4>2</h4> |
||||
|
<p>android.permission.READ_EXTERNAL_STORAGE</p> |
||||
|
<p>android.permission.WRITE_EXTERNAL_STORAGE</p> |
||||
|
<p>This permission is required when you access an application to perform application management functions.</p> |
||||
|
<p>Reminder method: User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<h4>3 Network permissions:</h4> |
||||
|
<p>android.permission.INTERNET</p> |
||||
|
<p>When you use These permissions are required for Voitrans's information services and version upgrade services, and in order to improve the user experience and optimize the service capabilities, third-party SDKS such as Friends will also use these permissions.</p> |
||||
|
<p>Reminder mode: User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<h4>4 Get network status permissions:</h4> |
||||
|
<p>android.permission.ACCESS_NETWORK_STATE</p> |
||||
|
<p>android.permission.ACCESS_WIFI_STATE</p> |
||||
|
<p>It is used to obtain whether the current network connection is valid, determine whether the current 4G environment is , and whether to remind the network environment changes, thus affecting the loading of information content. This permission is applied for when you use Voitrans.</p> |
||||
|
<p>Changes in the state of the network affect the uploading and loading of data. Therefore, the access to network status permission will be shared with friends + and other platforms.</p> |
||||
|
<p>Reminder method: User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<h4>5 Get task list permissions:</h4> |
||||
|
<p>android.permission.GET_TASKS</p> |
||||
|
<p>This permission is used to obtain the application list for the software management function. When you use the software management function, you need to read the applications installed on the mobile phone.</p> |
||||
|
<p>Reminder mode: User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<h4>6 Obtain MAC address permission</h4> |
||||
|
<p>android.permission.ACCESS_WIFI_STATE</p> |
||||
|
<p>android.permission.INTERNET</p> |
||||
|
<p>android.permission.ACCESS_NETWORK_STATE</p> |
||||
|
<p>When you use Voitrans, the application will need to obtain MAC address permissions:</p> |
||||
|
<p>When an application needs to report target conversion data, third-party platforms such as Youmon +SDK need to use MAC.</p> |
||||
|
<p>Reminder method: User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<h4>7 Obtain permission to record audio</h4> |
||||
|
<p>android.permission.RECORD_AUDIO</p> |
||||
|
<p>When you use the voice input function of Voitrans, you need to use the permission to record audio, and mainly use the voice input for translation and other related functions.</p> |
||||
|
<p>Reminder method: User Agreement and Privacy Policy description , the user dynamically pops the box when required.</p> |
||||
|
|
||||
|
<h4>8 Obtain SSID permission for WIFI</h4> |
||||
|
<p>When this permission is used, the app reads the SSID permission for WIFI:</p> |
||||
|
<p>Scope of use and purpose: When the application needs to use data analysis services, third-party platforms such as +SDK need to read the device's "WIFI SSID permission" to provide users with more accurate and effective functional services.</p> |
||||
|
<p>Reminder method: User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<h4>9 Get Bluetooth access</h4> |
||||
|
<p>When this permission is used, the app reads the Bluetooth permission:</p> |
||||
|
<p>Scope of use and purpose: Bluetooth devices for searching and configuring accessories, such as Bluetooth headsets, Bluetooth watches, etc. Easy for users to manage</p> |
||||
|
<p>Reminder method: A prompt indicating the permission to start the application for the first time is displayed, requiring the user to confirm acceptance. User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<h4>10 Get the BSSID permission for WiFi</h4> |
||||
|
<p>When the application needs to distinguish and identify the user to provide functional services and data statistics, it will read the "WiFi BSSID" permission of the device for:</p> |
||||
|
<p>Scope of use and purpose: 1 Third-party platforms: such as Youmeng SDK, Bugly SDK, Autonavi SDK</p> |
||||
|
<p>Reminder method: User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<h4>11 Obtain the application installation permission</h4> |
||||
|
<p>android.permission.REQUEST_INSTALL_PACKAGES</p> |
||||
|
<p>The software will use the application installation permission, which is mainly used to allow users to install a new version and experience new functions when there is a new version.</p> |
||||
|
<p>Reminder mode: User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<h4>12 Get permissions for the camera</h4> |
||||
|
<p>android.permission.CAMERA</p> |
||||
|
<p>Voitrans uses camera permissions to scan QR codes and bind Bluetooth devices such as Bluetooth headsets and watches.</p> |
||||
|
<p>Reminder method: A prompt indicating the permission to start the application for the first time is displayed, requiring the user to confirm acceptance. User Agreement and Privacy Policy description.</p> |
||||
|
|
||||
|
<p><strong>Special note:</strong></p> |
||||
|
<p>1 Application integration within their Allies SDK, Scott map SDK in the user agrees to allow after use, application in the background/foreground for ICCID, IMSI, IMEI, SN, ANDROID, BSSID, ANDROID ID, SSID, IMSI and other personal information;</p> |
||||
|
<p>Remarks: More Please see the following SDK-related content and permissions [VIII. Access to third-party SDKS list]</p> |
||||
|
|
||||
|
<h3>8. Access third-party SDKS</h3> |
||||
|
<p>1 In order to ensure the stable operation and functional realization of our client, so that you can use and enjoy more services and functions, our application will be embedded in the software tool development kit (SDK) or other similar applications of third parties, these third parties will collect and process relevant information in accordance with their own privacy policies, in order to normally provide functions and advertising services. You can learn about the information collection and processing rules of the third party in the following section. We will conduct strict security tests on the development kits (SDKS) for partners to obtain information, in order to protect the reasonable use and security of your personal information. Please note that third-party SDKS may change the data processing types due to their version upgrades. Please refer to the privacy policies published by third-party SDKS. See the details below or click to view the Third Party SDK List.</p> |
||||
|
|
||||
|
<h4>8.1 WeChat OpenSDK Android (com.tencent.mm.opensdk)</h4> |
||||
|
<p>Information Obtained: Device number, IP address, device software information, device identification code (IMEI, Android ID, IDFA, OAID, OPENUDID, GUID, ICCID, SIM card IMSI information, IP, MAC address), device identifier, region, device-related application information, and other service-related log information</p> |
||||
|
<p>Purpose of Use: WeChat login and sharing</p> |
||||
|
<p>Developer: Shenzhen Tencent Computer Systems Company Limited</p> |
||||
|
<p>Collection Method: SDK self-collection</p> |
||||
|
<p>Privacy Policy Link: https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/RYiYJkLOrQwu0nb8</p> |
||||
|
|
||||
|
<h4>8.2 Navigation SDK (com.amap.api.navi)</h4> |
||||
|
<p>Information Obtained: Application name, application version number, device brand and model, operating system, carrier information, screen resolution, sensor information (vector, acceleration, pressure, direction, geomagnetic, gyroscope, temperature, light sensor, NMEA signal), device signal strength information</p> |
||||
|
<p>Purpose of Use: Navigation using Amap</p> |
||||
|
<p>Developer: Beijing Gaode Tuqiang Technology Co., Ltd.</p> |
||||
|
<p>Collection Method: SDK self-collection</p> |
||||
|
<p>Privacy Policy Link: https://lbs.amap.com/pages/privacy/</p> |
||||
|
|
||||
|
<h4>8.3 Map SDK (com.amap.api.maps)</h4> |
||||
|
<p>Information Obtained: Application name, application version, device brand and model, operating system, network information, device signal strength, etc.</p> |
||||
|
<p>Purpose of Use: Map display and interaction</p> |
||||
|
<p>Developer: Beijing Gaode Tuqiang Technology Co., Ltd.</p> |
||||
|
<p>Collection Method: SDK self-collection</p> |
||||
|
<p>Privacy Policy Link: https://lbs.amap.com/pages/privacy/</p> |
||||
|
|
||||
|
<h4>8.4 Location SDK (com.amap.api.location)</h4> |
||||
|
<p>Information Obtained: Location information (GPS, Wi-Fi, base station), device information, network information, etc.</p> |
||||
|
<p>Purpose of Use: Provide location services</p> |
||||
|
<p>Developer: Beijing Gaode Tuqiang Technology Co., Ltd.</p> |
||||
|
<p>Collection Method: SDK self-collection</p> |
||||
|
<p>Privacy Policy Link: https://lbs.amap.com/pages/privacy/</p> |
||||
|
|
||||
|
<h4>8.5 Lite Map SDK (com.amap.api.maps.utils)</h4> |
||||
|
<p>Information Obtained: Application name, application version, device information, network information, etc.</p> |
||||
|
<p>Purpose of Use: Map tools and rendering optimization</p> |
||||
|
<p>Developer: Beijing Gaode Tuqiang Technology Co., Ltd.</p> |
||||
|
<p>Collection Method: SDK self-collection</p> |
||||
|
<p>Privacy Policy Link: https://lbs.amap.com/pages/privacy/</p> |
||||
|
|
||||
|
<h4>8.6 Alipay App Client SDK (com.alipay.sdk)</h4> |
||||
|
<p>Information Obtained: Device identifiers, basic device information, installed software list, and network information (see official privacy policy for details)</p> |
||||
|
<p>Purpose of Use: Alipay account login and payment invocation</p> |
||||
|
<p>Developer: Alipay (Hangzhou) Information Technology Co., Ltd.</p> |
||||
|
<p>Collection Method: SDK self-collection</p> |
||||
|
<p>Privacy Policy Link: https://opendocs.alipay.com/open/54/01g6qm#%E6%94%AF%E4%BB%98%E5%AE%9D%20App%20%E6%94%AF%E4%BB%98%E5%AE%A2%E6%88%B7%E7%AB%AF%20SDK%20%E9%9A%90%E7%A7%81%E6%94%BF%E7%AD%96</p> |
||||
|
|
||||
|
<h4>8.7 Beacon SDK Android (com.tencent.beacon)</h4> |
||||
|
<p>Information Obtained: SDK/API version, platform, timestamp, app identifier, app version, device manufacturer, OS version, locale, timezone, network status (Wi-Fi, etc.), network logs, crash and usage data, IP address, etc. (see official privacy policy for details)</p> |
||||
|
<p>Purpose of Use: App quality monitoring and data reporting service</p> |
||||
|
<p>Developer: Shenzhen Tencent Computer Systems Company Limited</p> |
||||
|
<p>Collection Method: SDK self-collection</p> |
||||
|
<p>Privacy Policy Link: https://privacy.qq.com/document/preview/a19a8cf4f2354306b137a7db39d32024</p> |
||||
|
|
||||
|
<h3>9. Protection of minors</h3> |
||||
|
<p>Users of our products and services may include Minors under the age of 18, minor users should seek the advice of their parents or guardians regarding this Privacy Policy before using our products, and use our services or provide your information to us after obtaining the consent of and under the guidance of their parents or guardians. Guardians of minors who have questions may contact us through the contact details in this Privacy Policy.</p> |
||||
|
<p>If you do not agree to the collection of the aforementioned information by the above third-party service providers, please stop using the Software immediately. During your use of the Software, these third parties will collect and process relevant information in accordance with their own privacy policies to provide some functions, advertising and other services. If the information is leaked due to the collection of the aforesaid information by the relevant third-party service provider, the relevant third-party service provider shall bear the corresponding legal responsibility.</p> |
||||
|
|
||||
|
<h3>10. Effective and updated Privacy Policy</h3> |
||||
|
<p>1. This Privacy policy is effective from the date of publication.</p> |
||||
|
<p>2. As our business evolves and changes, our Privacy Policy may change. We reserve the right to change our privacy policy at any time, so please check back often. If we make changes to our Privacy Policy, we will post these updates on our website, apps, and where we deem appropriate to keep you informed. If the privacy policy is updated, it will take effect from the date of update. Your continued use of our products or services means that you agree to our changed Privacy Policy.</p> |
||||
|
|
||||
|
<h3>11. How to contact us</h3> |
||||
|
<p>Saitong Industries Inc Established on November 22, 2024, registered address: 18305 E Valley Blvd, Unit C, City of Industry, CA 91744, USA. If you want to know more about our company and platform, you can contact us through the contact information provided in this policy (saitong.voitrans@gmail.com).</p> |
||||
|
</div> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict/> |
||||
|
</plist> |
||||
@ -0,0 +1,239 @@ |
|||||
|
/// 商品类型 |
||||
|
/// 0: 普通产品 normal |
||||
|
/// 1: 会员产品 member |
||||
|
import 'dart:convert'; |
||||
|
|
||||
|
// 使用量类型(与后端/Proto枚举对应) |
||||
|
enum UsageType { |
||||
|
/// 未知使用量类型 |
||||
|
UsageNull, // 0 |
||||
|
/// 翻译 |
||||
|
Translation, // 1 |
||||
|
/// 会议 |
||||
|
Meet, // 2 |
||||
|
/// 智能体 |
||||
|
AI, // 3 |
||||
|
/// 智能体 |
||||
|
Vip; // 4 |
||||
|
/// Vip |
||||
|
|
||||
|
// 解析支持 int 或 string(大小写不敏感) |
||||
|
static UsageType from(dynamic value) { |
||||
|
if (value is int) { |
||||
|
switch (value) { |
||||
|
case 0: |
||||
|
return UsageType.UsageNull; |
||||
|
case 1: |
||||
|
return UsageType.Translation; |
||||
|
case 2: |
||||
|
return UsageType.Meet; |
||||
|
case 3: |
||||
|
return UsageType.AI; |
||||
|
case 4: |
||||
|
return UsageType.Vip; |
||||
|
default: |
||||
|
return UsageType.UsageNull; |
||||
|
} |
||||
|
} |
||||
|
if (value is String) { |
||||
|
final v = int.tryParse(value.trim()); |
||||
|
if (v != null) return UsageType.from(v); |
||||
|
switch (value.trim().toLowerCase()) { |
||||
|
case 'usagenull': |
||||
|
case 'null': |
||||
|
case 'unknown': |
||||
|
return UsageType.UsageNull; |
||||
|
case 'translation': |
||||
|
return UsageType.Translation; |
||||
|
case 'meet': |
||||
|
case 'meeting': |
||||
|
return UsageType.Meet; |
||||
|
case 'ai': |
||||
|
return UsageType.AI; |
||||
|
case 'vip': |
||||
|
return UsageType.Vip; |
||||
|
default: |
||||
|
return UsageType.UsageNull; |
||||
|
} |
||||
|
} |
||||
|
return UsageType.UsageNull; |
||||
|
} |
||||
|
|
||||
|
int get toInt { |
||||
|
switch (this) { |
||||
|
case UsageType.UsageNull: |
||||
|
return 0; |
||||
|
case UsageType.Translation: |
||||
|
return 1; |
||||
|
case UsageType.Meet: |
||||
|
return 2; |
||||
|
case UsageType.AI: |
||||
|
return 3; |
||||
|
case UsageType.Vip: |
||||
|
return 4; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
String get name { |
||||
|
switch (this) { |
||||
|
case UsageType.UsageNull: |
||||
|
return 'UsageNull'; |
||||
|
case UsageType.Translation: |
||||
|
return 'Translation'; |
||||
|
case UsageType.Meet: |
||||
|
return 'Meet'; |
||||
|
case UsageType.AI: |
||||
|
return 'AI'; |
||||
|
case UsageType.Vip: |
||||
|
return 'Vip'; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 支付商品模型,与后端 DBGoods 对应 |
||||
|
/// - id: 产品id |
||||
|
/// - enable: 是否启用 |
||||
|
/// - name: 产品名称 |
||||
|
/// - desc: 产品描述 |
||||
|
/// - price: 产品价格(建议单位:分) |
||||
|
/// - usagetype: 产品类型 0:普通产品 1:会员产品 |
||||
|
/// - usagenum: 使用量(时长/次数/额度等) |
||||
|
class DBGoods { |
||||
|
final String id; |
||||
|
final bool enable; |
||||
|
final String name; |
||||
|
final String localname; |
||||
|
final int price; // 建议单位:分 |
||||
|
final UsageType usagetype; |
||||
|
final int usagenum; |
||||
|
|
||||
|
const DBGoods({ |
||||
|
required this.id, |
||||
|
required this.enable, |
||||
|
required this.name, |
||||
|
required this.localname, |
||||
|
required this.price, |
||||
|
required this.usagetype, |
||||
|
required this.usagenum, |
||||
|
}); |
||||
|
|
||||
|
/// 解析后端 JSON |
||||
|
factory DBGoods.fromJson(Map<String, dynamic> json) { |
||||
|
String _asString(dynamic v) { |
||||
|
if (v == null) return ''; |
||||
|
return v is String ? v : v.toString(); |
||||
|
} |
||||
|
|
||||
|
bool _asBool(dynamic v) { |
||||
|
if (v is bool) return v; |
||||
|
if (v is num) return v != 0; |
||||
|
if (v is String) { |
||||
|
final s = v.trim().toLowerCase(); |
||||
|
return s == 'true' || s == '1'; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
int _asInt(dynamic v) { |
||||
|
if (v == null) return 0; |
||||
|
if (v is int) return v; |
||||
|
if (v is num) return v.toInt(); |
||||
|
if (v is String) return int.tryParse(v) ?? 0; |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return DBGoods( |
||||
|
id: _asString(json['id']), |
||||
|
enable: _asBool(json['enable']), |
||||
|
name: _asString(json['name']), |
||||
|
localname: _asString(json['localname']), |
||||
|
price: _asInt(json['price']), |
||||
|
usagetype: UsageType.from(json['usagetype']), |
||||
|
usagenum: _asInt(json['usagenum']), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/// 编码为后端 JSON |
||||
|
Map<String, dynamic> toJson() { |
||||
|
return { |
||||
|
'id': id, |
||||
|
'enable': enable, |
||||
|
'name': name, |
||||
|
'localname': localname, |
||||
|
'price': price, |
||||
|
'usagetype': usagetype.toInt, |
||||
|
'usagenum': usagenum, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/// 列表解析 |
||||
|
static List<DBGoods> listFromJson(dynamic data) { |
||||
|
if (data == null) return const []; |
||||
|
if (data is List) { |
||||
|
return data |
||||
|
.map((e) => DBGoods.fromJson(e as Map<String, dynamic>)) |
||||
|
.toList(); |
||||
|
} |
||||
|
// 某些接口返回 { list: [...] } 或 { data: [...] } |
||||
|
if (data is Map<String, dynamic>) { |
||||
|
final list = data['list'] ?? data['data'] ?? data['items']; |
||||
|
if (list is List) { |
||||
|
return list |
||||
|
.map((e) => DBGoods.fromJson(e as Map<String, dynamic>)) |
||||
|
.toList(); |
||||
|
} |
||||
|
} |
||||
|
return const []; |
||||
|
} |
||||
|
|
||||
|
/// 价格换算:分 -> 元(仅当 price 单位为分时使用) |
||||
|
double get priceYuan => price / 100.0; |
||||
|
|
||||
|
/// 用于支付创建订单的金额(分) |
||||
|
int get payAmountCents => price; |
||||
|
|
||||
|
DBGoods copyWith({ |
||||
|
String? id, |
||||
|
bool? enable, |
||||
|
String? name, |
||||
|
String? localname, |
||||
|
int? price, |
||||
|
UsageType? usagetype, |
||||
|
int? usagenum, |
||||
|
}) { |
||||
|
return DBGoods( |
||||
|
id: id ?? this.id, |
||||
|
enable: enable ?? this.enable, |
||||
|
name: name ?? this.name, |
||||
|
localname: localname ?? this.localname, |
||||
|
price: price ?? this.price, |
||||
|
usagetype: usagetype ?? this.usagetype, |
||||
|
usagenum: usagenum ?? this.usagenum, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
String toString() { |
||||
|
return 'DBGoods(id: $id, enable: $enable, name: $name, price: $price, usagetype: ${usagetype.name}, usagenum: $usagenum)'; |
||||
|
} |
||||
|
|
||||
|
Map<String, dynamic>? get _localnameMap { |
||||
|
if (localname.isEmpty) return null; |
||||
|
try { |
||||
|
final v = jsonDecode(localname); |
||||
|
if (v is Map<String, dynamic>) return v; |
||||
|
} catch (_) {} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
Map<String, dynamic> localInfo(String tag) { |
||||
|
final m = _localnameMap; |
||||
|
if (m == null) return const {}; |
||||
|
final key1 = tag; |
||||
|
final key2 = tag.replaceAll('_', '-'); |
||||
|
final key3 = tag.replaceAll('-', '_'); |
||||
|
dynamic e = m[key1] ?? m[key2] ?? m[key3]; |
||||
|
if (e == null && m.isNotEmpty) e = m.values.first; |
||||
|
return e is Map<String, dynamic> ? e as Map<String, dynamic> : const {}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,292 @@ |
|||||
|
///会议模块用量统计的模型类 |
||||
|
|
||||
|
// ============== ASR 状态常量 ============== |
||||
|
const int asrStatusProcessing = 0; // 处理中 |
||||
|
const int asrStatusFailed = 1; // 处理失败 |
||||
|
const int asrStatusCompleted = 2; // 已完成 |
||||
|
|
||||
|
// ============== ASR 单次记录 ============== |
||||
|
// 会议音频文件转文字单次数据 |
||||
|
class MeetingAsrRecord { |
||||
|
String id; // 本次ASR唯一标识 |
||||
|
int meetingId; // 会议记录ID |
||||
|
String taskId; // 云端任务ID |
||||
|
int audioSeconds; // 音频总时长(秒) |
||||
|
int fileSizeBytes; // 音频文件大小(字节) |
||||
|
String language; // ASR语言 |
||||
|
int submitTimestamp; // 提交时间戳(毫秒) |
||||
|
|
||||
|
int status; // 状态: 0-处理中, 1-处理失败, 2-已完成 |
||||
|
int transcriptBytes; // 转写后的字节数(utf8) |
||||
|
int completeTimestamp; // 完成时间戳(毫秒) |
||||
|
|
||||
|
MeetingAsrRecord({ |
||||
|
required this.id, |
||||
|
required this.meetingId, |
||||
|
required this.taskId, |
||||
|
required this.audioSeconds, |
||||
|
required this.fileSizeBytes, |
||||
|
required this.language, |
||||
|
required this.submitTimestamp, |
||||
|
required this.status, |
||||
|
required this.transcriptBytes, |
||||
|
required this.completeTimestamp, |
||||
|
}); |
||||
|
|
||||
|
Map<String, dynamic> toJson() => { |
||||
|
'id': id, |
||||
|
'meetingId': meetingId, |
||||
|
'taskId': taskId, |
||||
|
'audioSeconds': audioSeconds, |
||||
|
'fileSizeBytes': fileSizeBytes, |
||||
|
'language': language, |
||||
|
'submitTimestamp': submitTimestamp, |
||||
|
'status': status, |
||||
|
'transcriptBytes': transcriptBytes, |
||||
|
'completeTimestamp': completeTimestamp, |
||||
|
}; |
||||
|
|
||||
|
factory MeetingAsrRecord.fromJson(Map<String, dynamic> json) { |
||||
|
return MeetingAsrRecord( |
||||
|
id: json['id'] ?? '', |
||||
|
meetingId: json['meetingId'] ?? 0, |
||||
|
taskId: json['taskId'] ?? '', |
||||
|
audioSeconds: json['audioSeconds'] ?? 0, |
||||
|
fileSizeBytes: json['fileSizeBytes'] ?? 0, |
||||
|
language: json['language'] ?? '', |
||||
|
submitTimestamp: json['submitTimestamp'] ?? 0, |
||||
|
status: json['status'] ?? 0, |
||||
|
transcriptBytes: json['transcriptBytes'] ?? 0, |
||||
|
completeTimestamp: json['completeTimestamp'] ?? 0, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ============== 对象存储单次记录 ============== |
||||
|
// 对象存储单次上传记录 |
||||
|
class MeetingObjectStorageRecord { |
||||
|
int meetingId; // 会议记录ID |
||||
|
String storageUrl; // 云端存储URL |
||||
|
int storageBytes; // 文件大小(字节) |
||||
|
int transferBytes; // 实际传输字节数 |
||||
|
int uploadTimestamp; // 上传完成时间戳(毫秒) |
||||
|
|
||||
|
MeetingObjectStorageRecord({ |
||||
|
required this.meetingId, |
||||
|
required this.storageUrl, |
||||
|
required this.storageBytes, |
||||
|
required this.transferBytes, |
||||
|
required this.uploadTimestamp, |
||||
|
}); |
||||
|
|
||||
|
Map<String, dynamic> toJson() => { |
||||
|
'meetingId': meetingId, |
||||
|
'storageUrl': storageUrl, |
||||
|
'storageBytes': storageBytes, |
||||
|
'transferBytes': transferBytes, |
||||
|
'uploadTimestamp': uploadTimestamp, |
||||
|
}; |
||||
|
|
||||
|
factory MeetingObjectStorageRecord.fromJson(Map<String, dynamic> json) { |
||||
|
int _asInt(dynamic v) { |
||||
|
if (v == null) return 0; |
||||
|
if (v is int) return v; |
||||
|
if (v is num) return v.toInt(); |
||||
|
if (v is String) return int.tryParse(v) ?? 0; |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return MeetingObjectStorageRecord( |
||||
|
meetingId: _asInt(json['meetingId']), |
||||
|
storageUrl: (json['storageUrl'] ?? '').toString(), |
||||
|
storageBytes: _asInt(json['storageBytes']), |
||||
|
transferBytes: _asInt(json['transferBytes']), |
||||
|
uploadTimestamp: _asInt(json['uploadTimestamp']), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 会议音频文件转文字单日统计 |
||||
|
class MeetingAsrDailyStats { |
||||
|
String date; // 日期字符串: YYYY-MM-DD |
||||
|
String userId; // 用户ID |
||||
|
List<MeetingAsrRecord> items; // 当天所有单次ASR |
||||
|
|
||||
|
MeetingAsrDailyStats({ |
||||
|
required this.date, |
||||
|
required this.userId, |
||||
|
required this.items, |
||||
|
}); |
||||
|
|
||||
|
// 计算型聚合,不冗余存储 |
||||
|
int get asrCalls => items.length; |
||||
|
int get audioSecondsTotal => items.fold(0, (s, r) => s + r.audioSeconds); |
||||
|
int get audioBytesTotal => items.fold(0, (s, r) => s + r.fileSizeBytes); |
||||
|
int get transcriptBytesTotal => |
||||
|
items.fold(0, (s, r) => s + r.transcriptBytes); |
||||
|
|
||||
|
Map<String, dynamic> toJson() => { |
||||
|
'date': date, |
||||
|
'userId': userId, |
||||
|
'items': items.map((e) => e.toJson()).toList(), |
||||
|
}; |
||||
|
|
||||
|
factory MeetingAsrDailyStats.fromJson(Map<String, dynamic> json) { |
||||
|
final list = (json['items'] as List? ?? []) |
||||
|
.map((e) => MeetingAsrRecord.fromJson(Map<String, dynamic>.from(e))) |
||||
|
.toList(); |
||||
|
return MeetingAsrDailyStats( |
||||
|
date: json['date'] ?? '', |
||||
|
userId: json['userId'] ?? 'anonymous', |
||||
|
items: list, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ============== 对象存储单日统计 ============== |
||||
|
// 👇 职责单一:仅统计对象存储相关数据 |
||||
|
class MeetingStorageDailyStats { |
||||
|
String date; // 日期字符串: YYYY-MM-DD |
||||
|
String userId; // 用户ID |
||||
|
List<MeetingObjectStorageRecord> items; // 当天所有单次对象存储记录 |
||||
|
|
||||
|
MeetingStorageDailyStats({ |
||||
|
required this.date, |
||||
|
required this.userId, |
||||
|
required this.items, |
||||
|
}); |
||||
|
|
||||
|
// 对象存储 聚合计算 |
||||
|
int get storageUploads => items.length; |
||||
|
int get storageBytesTotal => items.fold(0, (s, r) => s + r.storageBytes); |
||||
|
int get transferBytesTotal => items.fold(0, (s, r) => s + r.transferBytes); |
||||
|
|
||||
|
Map<String, dynamic> toJson() => { |
||||
|
'date': date, |
||||
|
'userId': userId, |
||||
|
'items': items.map((e) => e.toJson()).toList(), |
||||
|
}; |
||||
|
|
||||
|
factory MeetingStorageDailyStats.fromJson(Map<String, dynamic> json) { |
||||
|
final storageList = (json['items'] as List? ?? []) |
||||
|
.map((e) => |
||||
|
MeetingObjectStorageRecord.fromJson(Map<String, dynamic>.from(e))) |
||||
|
.toList(); |
||||
|
|
||||
|
return MeetingStorageDailyStats( |
||||
|
date: json['date'] ?? '', |
||||
|
userId: json['userId'] ?? 'anonymous', |
||||
|
items: storageList, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 会议音频文件转文字历史统计(所有的用量) |
||||
|
class MeetingAsrHistoryStats { |
||||
|
String userId; // 用户ID |
||||
|
int allAsrCalls; // 所有ASR调用次数 |
||||
|
int allAudioSeconds; // 所有音频总时长(秒) |
||||
|
int allAudioFileBytes; // 所有音频文件大小(字节) |
||||
|
int allTranscriptBytes; // 所有转写后的字节数(utf8) |
||||
|
|
||||
|
MeetingAsrHistoryStats({ |
||||
|
required this.userId, |
||||
|
required this.allAsrCalls, |
||||
|
required this.allAudioSeconds, |
||||
|
required this.allAudioFileBytes, |
||||
|
required this.allTranscriptBytes, |
||||
|
}); |
||||
|
|
||||
|
Map<String, dynamic> toJson() => { |
||||
|
'userId': userId, |
||||
|
'allAsrCalls': allAsrCalls, |
||||
|
'allAudioSeconds': allAudioSeconds, |
||||
|
'allAudioFileBytes': allAudioFileBytes, |
||||
|
'allTranscriptBytes': allTranscriptBytes, |
||||
|
}; |
||||
|
|
||||
|
factory MeetingAsrHistoryStats.fromJson(Map<String, dynamic> json) { |
||||
|
int _asInt(dynamic v) { |
||||
|
if (v == null) return 0; |
||||
|
if (v is int) return v; |
||||
|
if (v is num) return v.toInt(); |
||||
|
if (v is String) return int.tryParse(v) ?? 0; |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return MeetingAsrHistoryStats( |
||||
|
userId: json['userId'] ?? 'anonymous', |
||||
|
allAsrCalls: _asInt(json['allAsrCalls']), |
||||
|
allAudioSeconds: _asInt(json['allAudioSeconds']), |
||||
|
allAudioFileBytes: _asInt(json['allAudioFileBytes']), |
||||
|
allTranscriptBytes: _asInt(json['allTranscriptBytes']), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ============== 对象存储历史统计 ============== |
||||
|
// 👇 职责单一:仅统计对象存储累计数据 |
||||
|
class MeetingStorageHistoryStats { |
||||
|
String userId; // 用户ID |
||||
|
int allStorageUploads; // 所有对象存储上传次数 |
||||
|
int allStorageBytes; // 所有存储文件总大小(字节) |
||||
|
int allTransferBytes; // 所有传输总字节数 |
||||
|
|
||||
|
MeetingStorageHistoryStats({ |
||||
|
required this.userId, |
||||
|
required this.allStorageUploads, |
||||
|
required this.allStorageBytes, |
||||
|
required this.allTransferBytes, |
||||
|
}); |
||||
|
|
||||
|
Map<String, dynamic> toJson() => { |
||||
|
'userId': userId, |
||||
|
'allStorageUploads': allStorageUploads, |
||||
|
'allStorageBytes': allStorageBytes, |
||||
|
'allTransferBytes': allTransferBytes, |
||||
|
}; |
||||
|
|
||||
|
factory MeetingStorageHistoryStats.fromJson(Map<String, dynamic> json) { |
||||
|
int _asInt(dynamic v) { |
||||
|
if (v == null) return 0; |
||||
|
if (v is int) return v; |
||||
|
if (v is num) return v.toInt(); |
||||
|
if (v is String) return int.tryParse(v) ?? 0; |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return MeetingStorageHistoryStats( |
||||
|
userId: json['userId'] ?? 'anonymous', |
||||
|
allStorageUploads: _asInt(json['allStorageUploads']), |
||||
|
allStorageBytes: _asInt(json['allStorageBytes']), |
||||
|
allTransferBytes: _asInt(json['allTransferBytes']), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 👇 当需要同时展示两种统计时使用 |
||||
|
class MeetingUsageDailySnapshot { |
||||
|
String date; |
||||
|
String userId; |
||||
|
MeetingAsrDailyStats? asrStats; |
||||
|
MeetingStorageDailyStats? storageStats; |
||||
|
|
||||
|
MeetingUsageDailySnapshot({ |
||||
|
required this.date, |
||||
|
required this.userId, |
||||
|
this.asrStats, |
||||
|
this.storageStats, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
class MeetingUsageHistorySnapshot { |
||||
|
String userId; |
||||
|
MeetingAsrHistoryStats? asrStats; |
||||
|
MeetingStorageHistoryStats? storageStats; |
||||
|
|
||||
|
MeetingUsageHistorySnapshot({ |
||||
|
required this.userId, |
||||
|
this.asrStats, |
||||
|
this.storageStats, |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,105 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:get_storage/get_storage.dart'; |
||||
|
|
||||
|
import 'meeting_task_service.dart'; |
||||
|
import 'meeting_task_service2.dart'; |
||||
|
|
||||
|
/// 会议助手任务服务聚合器(统一列表与状态) |
||||
|
class MeetingTaskAggregator extends GetxService { |
||||
|
final GetStorage _storage = GetStorage(); |
||||
|
|
||||
|
final MeetingTaskService s1 = Get.find<MeetingTaskService>(); |
||||
|
final MeetingTaskService2 s2 = Get.find<MeetingTaskService2>(); |
||||
|
|
||||
|
RxList taskFinishList = [].obs; |
||||
|
RxInt tasktype = 1.obs; |
||||
|
int meetingId = 0; |
||||
|
List textList = []; |
||||
|
|
||||
|
Worker? _w1; |
||||
|
Worker? _w2; |
||||
|
Worker? _wTasktype; |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
_w1 = ever(s1.taskFinishList, (_) => _rebuild()); |
||||
|
_w2 = ever(s2.taskFinishList, (_) => _rebuild()); |
||||
|
_rebuild(); |
||||
|
} |
||||
|
|
||||
|
void setMeeting(int id) { |
||||
|
meetingId = id; |
||||
|
final String svc = |
||||
|
(_storage.read("meeting_service_type_$id") ?? '').toString(); |
||||
|
final bool isFast = svc == 'fast'; |
||||
|
_wTasktype?.dispose(); |
||||
|
if (isFast) { |
||||
|
_wTasktype = ever(s2.tasktype, (value) { |
||||
|
tasktype.value = value; |
||||
|
if (value == 2) { |
||||
|
textList = s2.textList; |
||||
|
} |
||||
|
}); |
||||
|
} else { |
||||
|
_wTasktype = ever(s1.tasktype, (value) { |
||||
|
tasktype.value = value; |
||||
|
if (value == 2) { |
||||
|
textList = s1.textList; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void clearMeeting() { |
||||
|
meetingId = 0; |
||||
|
_wTasktype?.dispose(); |
||||
|
_wTasktype = null; |
||||
|
} |
||||
|
|
||||
|
void _rebuild() { |
||||
|
final Set<int> ids = {}; |
||||
|
for (var e in s1.taskFinishList) { |
||||
|
if (e is Map && e['id'] is int) ids.add(e['id'] as int); |
||||
|
} |
||||
|
for (var e in s2.taskFinishList) { |
||||
|
if (e is Map && e['id'] is int) ids.add(e['id'] as int); |
||||
|
} |
||||
|
final List unified = []; |
||||
|
for (final id in ids) { |
||||
|
final String svc = |
||||
|
(_storage.read("meeting_service_type_$id") ?? '').toString(); |
||||
|
final Map a = s1.taskFinishList.firstWhere( |
||||
|
(x) => x['id'] == id, |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
final Map b = s2.taskFinishList.firstWhere( |
||||
|
(x) => x['id'] == id, |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
int va = a.isNotEmpty ? ((a['tasktype'] ?? 0) as int) : -999; |
||||
|
int vb = b.isNotEmpty ? ((b['tasktype'] ?? 0) as int) : -999; |
||||
|
int v; |
||||
|
if (svc == 'fast') { |
||||
|
if (va != -999 && vb != -999) { |
||||
|
v = va >= vb ? va : vb; |
||||
|
} else { |
||||
|
v = vb != -999 ? vb : va; |
||||
|
} |
||||
|
} else if (svc == 'standard') { |
||||
|
if (va != -999 && vb != -999) { |
||||
|
v = va >= vb ? va : vb; |
||||
|
} else { |
||||
|
v = va != -999 ? va : vb; |
||||
|
} |
||||
|
} else { |
||||
|
if (va == -999 && vb == -999) { |
||||
|
continue; |
||||
|
} |
||||
|
v = va == -999 ? vb : (vb == -999 ? va : (va >= vb ? va : vb)); |
||||
|
} |
||||
|
unified.add({'id': id, 'tasktype': v}); |
||||
|
} |
||||
|
taskFinishList.value = unified; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,971 @@ |
|||||
|
import 'dart:convert'; |
||||
|
import 'dart:io'; |
||||
|
|
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:get_storage/get_storage.dart'; |
||||
|
import 'package:http/http.dart' as http; |
||||
|
|
||||
|
import '../db/sqflite_api.dart'; |
||||
|
import '../../../data/models/appconfig.dart'; |
||||
|
import '../../../core/utils/logger.dart'; |
||||
|
import '../../../core/utils/synchrodata.dart'; |
||||
|
import '../usage_stats_service.dart'; |
||||
|
import '../../../data/models/meeting_usage_stats.dart'; |
||||
|
import '../deapsound_ai_service.dart'; |
||||
|
import 'meeting_upload_service.dart'; |
||||
|
// 移除标准版服务桥接,改为通过聚合器对外暴露统一状态 |
||||
|
import 'summary_task_service.dart'; |
||||
|
|
||||
|
//火山引擎录音文件识别大模型-极速版 |
||||
|
class MeetingTaskService2 extends GetxService { |
||||
|
final GetStorage _storage = GetStorage(); |
||||
|
|
||||
|
MeetingUploadService get _uploadService => Get.find<MeetingUploadService>(); |
||||
|
SummaryTaskService get _summaryTaskService => Get.find<SummaryTaskService>(); |
||||
|
|
||||
|
List _taskList = []; |
||||
|
RxList taskFinishList = [].obs; |
||||
|
|
||||
|
bool _isTask = false; |
||||
|
|
||||
|
String _systemPrompt = ''; |
||||
|
String _userId = ''; |
||||
|
|
||||
|
late final String _appid; |
||||
|
late final String _token; |
||||
|
late final String _serviceUrl; |
||||
|
|
||||
|
int meetingId = 0; |
||||
|
RxInt tasktype = 1.obs; |
||||
|
List textList = []; |
||||
|
String summary = ''; |
||||
|
String overview = ''; |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
_getAgent(); |
||||
|
_getTaskList(); |
||||
|
} |
||||
|
|
||||
|
void _getAgent() async { |
||||
|
Map userInfo = _storage.read("user_info"); |
||||
|
_userId = userInfo['user']['uid']; |
||||
|
_systemPrompt = AppConfig.isInitialized() |
||||
|
? (AppConfig.agentsystemPrompt("meet_assistant") ?? '') |
||||
|
: ''; |
||||
|
final String base = AppConfig.env('VOLC_OPENSPEECH_BASE_URL_BIGMODEL') ?? |
||||
|
'https://openspeech.bytedance.com/api/v3/auc/bigmodel'; |
||||
|
_appid = AppConfig.env('VOLC_OPENSPEECH_APP_ID') ?? ''; |
||||
|
_token = AppConfig.env('VOLC_OPENSPEECH_ACCESS_TOKEN') ?? ''; |
||||
|
_serviceUrl = |
||||
|
base.endsWith('/recognize/flash') ? base : (base + '/recognize/flash'); |
||||
|
} |
||||
|
|
||||
|
void _getTaskList() { |
||||
|
_taskList = _storage.read("meeting_task_list_v2") ?? []; |
||||
|
taskFinishList.value = _storage.read("meeting_task_finish_list_v2") ?? []; |
||||
|
_executeTask(); |
||||
|
} |
||||
|
|
||||
|
Future<void> addTask( |
||||
|
int id, |
||||
|
String audiourl, { |
||||
|
bool isSpeaker = false, |
||||
|
String language = '', |
||||
|
String targetLanguage = '', |
||||
|
String prompt = '', |
||||
|
String outline = '', |
||||
|
}) async { |
||||
|
if (audiourl.isEmpty) { |
||||
|
Map uploadData = _uploadService.uploadList.firstWhere( |
||||
|
(element) => element['id'] == id, |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
if (uploadData.isNotEmpty) { |
||||
|
if (uploadData['audiourl'].isEmpty) { |
||||
|
uploadData['isTask'] = true; |
||||
|
uploadData['template'] = { |
||||
|
'isSpeaker': isSpeaker, |
||||
|
'language': language, |
||||
|
'targetLanguage': targetLanguage, |
||||
|
'prompt': prompt, |
||||
|
'outline': outline, |
||||
|
}; |
||||
|
_storage.write("meeting_upload_list", _uploadService.uploadList); |
||||
|
} else { |
||||
|
await addTaskList( |
||||
|
id, |
||||
|
uploadData['audiourl'], |
||||
|
isSpeaker: isSpeaker, |
||||
|
language: language, |
||||
|
targetLanguage: targetLanguage, |
||||
|
prompt: prompt, |
||||
|
outline: outline, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
await addTaskList( |
||||
|
id, |
||||
|
audiourl, |
||||
|
isSpeaker: isSpeaker, |
||||
|
language: language, |
||||
|
targetLanguage: targetLanguage, |
||||
|
prompt: prompt, |
||||
|
outline: outline, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Future<void> addTaskList( |
||||
|
int id, |
||||
|
String audiourl, { |
||||
|
bool isSpeaker = false, |
||||
|
String language = '', |
||||
|
String targetLanguage = '', |
||||
|
String prompt = '', |
||||
|
String outline = '', |
||||
|
}) async { |
||||
|
final String taskId = DateTime.now().millisecondsSinceEpoch.toString(); |
||||
|
await SqfliteApi.editMeeting(id, {'tasktype': 1, 'taskid': taskId}); |
||||
|
|
||||
|
try { |
||||
|
final detail = await SqfliteApi.getMeetingDetails(id); |
||||
|
final seconds = (detail['seconds'] ?? 0) as int; |
||||
|
final filepath = (detail['filepath'] ?? '') as String; |
||||
|
int fileSizeBytes = 0; |
||||
|
if (filepath.isNotEmpty) { |
||||
|
try { |
||||
|
fileSizeBytes = await File(filepath).length(); |
||||
|
} catch (_) {} |
||||
|
} |
||||
|
final record = MeetingAsrRecord( |
||||
|
id: taskId, |
||||
|
meetingId: id, |
||||
|
taskId: taskId, |
||||
|
audioSeconds: seconds, |
||||
|
fileSizeBytes: fileSizeBytes, |
||||
|
language: language, |
||||
|
submitTimestamp: DateTime.now().millisecondsSinceEpoch, |
||||
|
status: asrStatusProcessing, |
||||
|
transcriptBytes: 0, |
||||
|
completeTimestamp: 0, |
||||
|
); |
||||
|
try { |
||||
|
final svc = Get.isRegistered<UsageStatsService>() |
||||
|
? Get.find<UsageStatsService>() |
||||
|
: Get.put(UsageStatsService(), permanent: true); |
||||
|
svc.addMeetingAsrRecord(record); |
||||
|
} catch (e) { |
||||
|
Logger.w('MeetingASRrecord', '提交用量记录失败: ' + e.toString()); |
||||
|
} |
||||
|
} catch (_) {} |
||||
|
_taskList.add({ |
||||
|
'id': id, |
||||
|
'audiourl': audiourl, |
||||
|
'tasktype': 1, |
||||
|
'taskid': taskId, |
||||
|
'template': { |
||||
|
'isSpeaker': isSpeaker, |
||||
|
'language': language, |
||||
|
'targetLanguage': targetLanguage, |
||||
|
'prompt': prompt, |
||||
|
'outline': outline, |
||||
|
}, |
||||
|
}); |
||||
|
taskFinishList.add({'id': id, 'tasktype': 1}); |
||||
|
_storage.write("meeting_task_list_v2", _taskList); |
||||
|
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
||||
|
// 由聚合器统一对外呈现状态,不直接写入标准版列表 |
||||
|
_executeTask(); |
||||
|
} |
||||
|
|
||||
|
void _executeTask() async { |
||||
|
if (_isTask) return; |
||||
|
_isTask = true; |
||||
|
Logger.debug('开始执行任务,任务数量:${_taskList.length}'); |
||||
|
|
||||
|
while (_taskList.isNotEmpty) { |
||||
|
Logger.debug('任务循环执行,剩余任务:${_taskList.length}'); |
||||
|
await Future.delayed(const Duration(seconds: 2)); |
||||
|
|
||||
|
// 创建任务列表的副本,避免在迭代过程中修改原列表 |
||||
|
List tasksCopy = List.from(_taskList); |
||||
|
|
||||
|
for (var element in tasksCopy) { |
||||
|
Logger.debug('处理任务:${element['id']},音频:${element['audiourl']}'); |
||||
|
|
||||
|
// 检查任务是否仍在列表中(可能已被其他地方移除) |
||||
|
if (_taskList.any((task) => task['id'] == element['id'])) { |
||||
|
try { |
||||
|
await _fileRecognize(element); |
||||
|
} catch (e) { |
||||
|
Logger.error('处理任务失败:${element['id']},错误:$e'); |
||||
|
// 确保失败的任务被移除,避免无限循环 |
||||
|
_taskList.removeWhere((task) => task['id'] == element['id']); |
||||
|
_storage.write("meeting_task_list_v2", _taskList); |
||||
|
|
||||
|
// 更新任务状态为失败 |
||||
|
await SqfliteApi.editMeeting(element['id'], {'tasktype': 0}); |
||||
|
} |
||||
|
} else { |
||||
|
Logger.debug('任务 ${element['id']} 已从任务列表移除'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 如果没有任务被处理,避免无限循环 |
||||
|
if (_taskList.length == tasksCopy.length) { |
||||
|
Logger.warning('本次循环未处理任何任务,为避免无限执行终止循环'); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_isTask = false; |
||||
|
Logger.debug('任务执行完成,剩余任务:${_taskList.length}'); |
||||
|
} |
||||
|
|
||||
|
void removeTask(int id, {bool isDeleteTask = false}) { |
||||
|
Map taskFinishData = taskFinishList.firstWhere( |
||||
|
(element) => element['id'] == id, |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
if (taskFinishData.isNotEmpty) { |
||||
|
if (isDeleteTask || taskFinishData['tasktype'] >= 3) { |
||||
|
taskFinishList.remove(taskFinishData); |
||||
|
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
||||
|
} |
||||
|
} |
||||
|
if (isDeleteTask) { |
||||
|
_taskList.removeWhere((element) => element['id'] == id); |
||||
|
_storage.write("meeting_task_list_v2", _taskList); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Future<Map<String, dynamic>> _recognizeAudio( |
||||
|
String audiourl, { |
||||
|
bool enableSpeakerInfo = true, |
||||
|
String language = '', |
||||
|
String? requestIdOverride, |
||||
|
}) async { |
||||
|
String requestId = |
||||
|
requestIdOverride ?? DateTime.now().millisecondsSinceEpoch.toString(); |
||||
|
|
||||
|
// 为每次请求生成新的headers - 适配大模型极速版API |
||||
|
Map<String, String> requestHeaders = { |
||||
|
'X-Api-App-Key': _appid, |
||||
|
'X-Api-Access-Key': _token, |
||||
|
'X-Api-Resource-Id': 'volc.bigasr.auc_turbo', |
||||
|
'X-Api-Request-Id': requestId, |
||||
|
'X-Api-Sequence': '-1', |
||||
|
'Content-Type': 'application/json', |
||||
|
}; |
||||
|
|
||||
|
// 使用大模型极速版API的新请求格式 |
||||
|
final Map<String, dynamic> requestPayload = { |
||||
|
"user": {"uid": _appid}, |
||||
|
"audio": {"url": audiourl}, |
||||
|
"request": { |
||||
|
"model_name": "bigmodel", |
||||
|
"enable_itn": true, |
||||
|
"enable_punc": true, |
||||
|
"enable_ddc": true, |
||||
|
"enable_speaker_info": enableSpeakerInfo, |
||||
|
"word_info": 1, |
||||
|
"show_utterances": true |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
if (language.isNotEmpty) { |
||||
|
requestPayload['request']['language'] = language; |
||||
|
} |
||||
|
|
||||
|
final String requestBody = jsonEncode(requestPayload); |
||||
|
|
||||
|
try { |
||||
|
// 记录请求详情 |
||||
|
Logger.debug('API 请求地址:$_serviceUrl'); |
||||
|
Logger.debug('API 请求头:$requestHeaders'); |
||||
|
Logger.debug('API 请求体:$requestBody'); |
||||
|
|
||||
|
final response = await http.post( |
||||
|
Uri.parse(_serviceUrl), |
||||
|
headers: requestHeaders, |
||||
|
body: requestBody, |
||||
|
); |
||||
|
|
||||
|
Logger.debug('API 响应状态码:${response.statusCode}'); |
||||
|
Logger.debug('API 响应头:${response.headers}'); |
||||
|
|
||||
|
if (response.statusCode != 200) { |
||||
|
Logger.error('识别失败,状态码:${response.statusCode},响应体:${response.body}'); |
||||
|
throw Exception('识别失败,状态码:${response.statusCode}'); |
||||
|
} |
||||
|
|
||||
|
final respDic = jsonDecode(response.body); |
||||
|
Logger.debug('识别接口响应:$respDic'); |
||||
|
|
||||
|
// 检查响应中是否包含资源相关信息 |
||||
|
if (respDic.containsKey('resource_id') || respDic.containsKey('model')) { |
||||
|
Logger.debug('资源校验 - 使用的资源ID:volc.bigasr.auc_turbo'); |
||||
|
Logger.debug( |
||||
|
'响应中的资源信息:${respDic['resource_id'] ?? respDic['model'] ?? '响应未包含'}'); |
||||
|
} |
||||
|
|
||||
|
return respDic; |
||||
|
} catch (e) { |
||||
|
Logger.error('识别请求异常:${e.toString()}'); |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
String _getFileType(String path) { |
||||
|
List<String> array = path.split('.'); |
||||
|
return array[array.length - 1]; |
||||
|
} |
||||
|
|
||||
|
int _parseTime(dynamic timeValue) { |
||||
|
if (timeValue == null) return 0; |
||||
|
|
||||
|
if (timeValue is int) { |
||||
|
return timeValue; |
||||
|
} else if (timeValue is double) { |
||||
|
return timeValue.round(); |
||||
|
} else if (timeValue is String) { |
||||
|
try { |
||||
|
double parsed = double.parse(timeValue); |
||||
|
return parsed.round(); |
||||
|
} catch (e) { |
||||
|
Logger.error('时间值解析失败:$timeValue'); |
||||
|
return 0; |
||||
|
} |
||||
|
} else { |
||||
|
Logger.error('未知的时间值类型:${timeValue.runtimeType},值:$timeValue'); |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Future<void> _fileRecognize(Map item) async { |
||||
|
try { |
||||
|
Logger.debug('开始识别音频:${item['audiourl']}'); |
||||
|
// 识别语言选取:若设置了目标输出语言且与录音语言一致,优先让ASR自动识别,避免错配 |
||||
|
final String tplLang = |
||||
|
(((item['template'] ?? {})['language']) ?? '').toString(); |
||||
|
final String tplTarget = |
||||
|
(((item['template'] ?? {})['targetLanguage']) ?? '').toString(); |
||||
|
String asrLanguage = tplLang; |
||||
|
if (tplTarget.isNotEmpty && tplTarget == tplLang) { |
||||
|
asrLanguage = ''; |
||||
|
} |
||||
|
var respDic = await _recognizeAudio( |
||||
|
item['audiourl'], |
||||
|
enableSpeakerInfo: |
||||
|
(((item['template'] ?? {})['isSpeaker']) ?? false) as bool, |
||||
|
language: asrLanguage, |
||||
|
requestIdOverride: (item['taskid'] ?? '').toString().isEmpty |
||||
|
? null |
||||
|
: (item['taskid'] ?? '').toString(), |
||||
|
); |
||||
|
|
||||
|
Logger.debug('API 全量响应结构:${respDic.toString()}'); |
||||
|
Logger.debug('响应包含的键:${respDic.keys.toList()}'); |
||||
|
|
||||
|
// 检查响应格式并提取utterances |
||||
|
List utterances = []; |
||||
|
|
||||
|
// 检查不同可能的响应格式 |
||||
|
if (respDic['result'] != null && |
||||
|
respDic['result']['utterances'] != null) { |
||||
|
utterances = respDic['result']['utterances']; |
||||
|
Logger.debug('在 result.utterances 中找到 ${utterances.length} 条语句'); |
||||
|
} else if (respDic['resp'] != null && |
||||
|
respDic['resp']['utterances'] != null) { |
||||
|
utterances = respDic['resp']['utterances']; |
||||
|
Logger.debug('在 resp.utterances 中找到 ${utterances.length} 条语句'); |
||||
|
} else if (respDic['utterances'] != null) { |
||||
|
utterances = respDic['utterances']; |
||||
|
Logger.debug('在根级 utterances 中找到 ${utterances.length} 条语句'); |
||||
|
} else if (respDic['data'] != null && |
||||
|
respDic['data']['utterances'] != null) { |
||||
|
utterances = respDic['data']['utterances']; |
||||
|
Logger.debug('在 data.utterances 中找到 ${utterances.length} 条语句'); |
||||
|
} else if (respDic['response'] != null && |
||||
|
respDic['response']['utterances'] != null) { |
||||
|
utterances = respDic['response']['utterances']; |
||||
|
Logger.debug('在 response.utterances 中找到 ${utterances.length} 条语句'); |
||||
|
} else { |
||||
|
// 检查是否有错误信息 |
||||
|
if (respDic['error'] != null) { |
||||
|
Logger.error('接口返回错误:${respDic['error']}'); |
||||
|
} else if (respDic['message'] != null) { |
||||
|
Logger.error('接口返回消息:${respDic['message']}'); |
||||
|
} else { |
||||
|
Logger.error('未知的响应格式。可用键:${respDic.keys.toList()}'); |
||||
|
// 尝试查找任何包含文本的字段 |
||||
|
respDic.forEach((key, value) { |
||||
|
Logger.debug('键:$key,值类型:${value.runtimeType},值:$value'); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (utterances.isNotEmpty) { |
||||
|
Logger.debug('开始处理 ${utterances.length} 条语句'); |
||||
|
await _addTextList(item['id'], utterances, item['template']); |
||||
|
// 从任务列表中移除已完成的任务 |
||||
|
_taskList.removeWhere((element) => element['id'] == item['id']); |
||||
|
_storage.write("meeting_task_list_v2", _taskList); |
||||
|
Logger.debug('音频识别成功,会议ID:${item['id']}'); |
||||
|
|
||||
|
// 更新UI状态 - 如果是当前会议,触发UI更新 |
||||
|
if (item['id'] == meetingId) { |
||||
|
tasktype.value = 2; // 转写完成,进入分析阶段 |
||||
|
} |
||||
|
} else { |
||||
|
Logger.error('响应中未发现语句。完整响应:$respDic'); |
||||
|
Logger.debug('未检测到语句,开始执行无声音频检查'); |
||||
|
Logger.debug('无声音频检测调试信息'); |
||||
|
Logger.debug('响应键列表:${respDic.keys.toList()}'); |
||||
|
Logger.debug('响应代码:${respDic['code']}'); |
||||
|
Logger.debug('响应消息:${respDic['message']}'); |
||||
|
|
||||
|
// 检查是否为无声音频 |
||||
|
bool isSilentAudio = _checkIfSilentAudio(respDic); |
||||
|
Logger.debug('无声检测结果:$isSilentAudio'); |
||||
|
|
||||
|
if (isSilentAudio) { |
||||
|
Logger.debug('检测到无声音频,设置任务类型为 -1'); |
||||
|
// 设置无声音频状态 (使用特殊值 -1) |
||||
|
await SqfliteApi.editMeeting(item['id'], {'tasktype': -1}); |
||||
|
if (item['id'] == meetingId) { |
||||
|
Logger.debug('当前会议设置 tasktype.value 为 -1'); |
||||
|
tasktype.value = -1; // 无声音频 |
||||
|
} |
||||
|
|
||||
|
// 更新taskFinishList中的状态 |
||||
|
Map taskFinishData = taskFinishList.firstWhere( |
||||
|
(element) => element['id'] == item['id'], |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
if (taskFinishData.isNotEmpty) { |
||||
|
taskFinishData['tasktype'] = -1; |
||||
|
taskFinishList.refresh(); |
||||
|
} |
||||
|
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
||||
|
// 由聚合器统一对外呈现状态,不直接写入标准版列表 |
||||
|
// 由聚合器统一对外呈现状态,不直接写入标准版列表 |
||||
|
} else { |
||||
|
Logger.error('识别失败,原因未知'); |
||||
|
// 识别失败,更新数据库状态为失败 |
||||
|
await SqfliteApi.editMeeting(item['id'], {'tasktype': 0}); |
||||
|
if (item['id'] == meetingId) { |
||||
|
tasktype.value = 0; // 识别失败 |
||||
|
} |
||||
|
|
||||
|
// 更新taskFinishList中的状态 |
||||
|
Map taskFinishData = taskFinishList.firstWhere( |
||||
|
(element) => element['id'] == item['id'], |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
if (taskFinishData.isNotEmpty) { |
||||
|
taskFinishData['tasktype'] = 0; |
||||
|
taskFinishList.refresh(); |
||||
|
} |
||||
|
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
||||
|
} |
||||
|
|
||||
|
// 从任务列表中移除 |
||||
|
_taskList.removeWhere((element) => element['id'] == item['id']); |
||||
|
_storage.write("meeting_task_list_v2", _taskList); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.error('文件识别流程发生异常:${e.toString()}'); |
||||
|
Logger.error('堆栈信息:${StackTrace.current}'); |
||||
|
|
||||
|
// 更新数据库状态为失败 |
||||
|
try { |
||||
|
await SqfliteApi.editMeeting(item['id'], {'tasktype': 0}); |
||||
|
} catch (dbError) { |
||||
|
Logger.error('数据库更新会议状态失败:$dbError'); |
||||
|
} |
||||
|
|
||||
|
// 出错时也要从任务列表中移除,避免无限重试 |
||||
|
_taskList.removeWhere((element) => element['id'] == item['id']); |
||||
|
_storage.write("meeting_task_list_v2", _taskList); |
||||
|
|
||||
|
// 更新UI状态 - 如果是当前会议,显示错误状态 |
||||
|
if (item['id'] == meetingId) { |
||||
|
tasktype.value = 0; // 识别失败 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 检查是否为无声音频 |
||||
|
bool _checkIfSilentAudio(Map<String, dynamic> response) { |
||||
|
Logger.debug('检查是否为无声音频,响应:${response.toString()}'); |
||||
|
|
||||
|
// 检查API响应中的错误信息或状态码 |
||||
|
// 根据实际API响应格式调整检测逻辑 |
||||
|
|
||||
|
// 检查是否有错误码表示无声音频 |
||||
|
if (response['code'] != null) { |
||||
|
int code = response['code']; |
||||
|
Logger.debug('检测到响应代码:$code'); |
||||
|
// 常见的无声音频错误码 |
||||
|
if (code == 40001 || |
||||
|
code == 40002 || |
||||
|
code == 40003 || |
||||
|
code == 50001 || |
||||
|
code == 50002 || |
||||
|
code == 50003) { |
||||
|
Logger.debug('根据错误码判断为无声音频:$code'); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检查错误消息 |
||||
|
if (response['message'] != null) { |
||||
|
String message = response['message'].toString().toLowerCase(); |
||||
|
Logger.debug('检测到响应消息:$message'); |
||||
|
if (message.contains('silent') || |
||||
|
message.contains('no audio') || |
||||
|
message.contains('无声') || |
||||
|
message.contains('静音') || |
||||
|
message.contains('empty audio') || |
||||
|
message.contains('no speech') || |
||||
|
message.contains('audio too short') || |
||||
|
message.contains('insufficient audio')) { |
||||
|
Logger.debug('根据消息判断为无声音频:$message'); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检查result中的错误信息 |
||||
|
if (response['result'] != null && response['result']['error'] != null) { |
||||
|
String error = response['result']['error'].toString().toLowerCase(); |
||||
|
Logger.debug('检测到 result.error:$error'); |
||||
|
if (error.contains('silent') || |
||||
|
error.contains('no audio') || |
||||
|
error.contains('无声') || |
||||
|
error.contains('静音') || |
||||
|
error.contains('empty audio') || |
||||
|
error.contains('no speech') || |
||||
|
error.contains('audio too short')) { |
||||
|
Logger.debug('根据 result.error 判断为无声音频:$error'); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检查是否有音频时长信息且时长为0或极短 |
||||
|
if (response['result'] != null && response['result']['duration'] != null) { |
||||
|
double duration = (response['result']['duration'] as num).toDouble(); |
||||
|
Logger.debug('检测到音频时长:$duration 秒'); |
||||
|
if (duration <= 0.5) { |
||||
|
// 音频时长小于0.5秒认为是无声 |
||||
|
Logger.debug('根据时长过短判断为无声音频:$duration'); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检查utterances是否为空且没有其他明确的成功标识 |
||||
|
bool hasUtterances = false; |
||||
|
if (response['result'] != null && |
||||
|
response['result']['utterances'] != null) { |
||||
|
hasUtterances = (response['result']['utterances'] as List).isNotEmpty; |
||||
|
} else if (response['resp'] != null && |
||||
|
response['resp']['utterances'] != null) { |
||||
|
hasUtterances = (response['resp']['utterances'] as List).isNotEmpty; |
||||
|
} else if (response['utterances'] != null) { |
||||
|
hasUtterances = (response['utterances'] as List).isNotEmpty; |
||||
|
} else if (response['data'] != null && |
||||
|
response['data']['utterances'] != null) { |
||||
|
hasUtterances = (response['data']['utterances'] as List).isNotEmpty; |
||||
|
} else if (response['response'] != null && |
||||
|
response['response']['utterances'] != null) { |
||||
|
hasUtterances = (response['response']['utterances'] as List).isNotEmpty; |
||||
|
} |
||||
|
|
||||
|
// 检查文本内容是否为空 |
||||
|
String textContent = ''; |
||||
|
if (response['result'] != null && response['result']['text'] != null) { |
||||
|
textContent = response['result']['text'].toString().trim(); |
||||
|
} else if (response['text'] != null) { |
||||
|
textContent = response['text'].toString().trim(); |
||||
|
} else if (response['data'] != null && response['data']['text'] != null) { |
||||
|
textContent = response['data']['text'].toString().trim(); |
||||
|
} |
||||
|
|
||||
|
// 如果文本内容为空且音频时长大于1秒,很可能是无声音频 |
||||
|
if (textContent.isEmpty) { |
||||
|
double audioDuration = 0; |
||||
|
if (response['audio_info'] != null && |
||||
|
response['audio_info']['duration'] != null) { |
||||
|
audioDuration = (response['audio_info']['duration'] as num).toDouble() / |
||||
|
1000; // 转换为秒 |
||||
|
} else if (response['result'] != null && |
||||
|
response['result']['additions'] != null && |
||||
|
response['result']['additions']['duration'] != null) { |
||||
|
audioDuration = |
||||
|
(response['result']['additions']['duration'] as num).toDouble() / |
||||
|
1000; // 转换为秒 |
||||
|
} |
||||
|
|
||||
|
Logger.debug('文本内容为空,音频时长:$audioDuration 秒'); |
||||
|
if (audioDuration > 1.0) { |
||||
|
// 音频时长大于1秒但没有文本内容,认为是无声音频 |
||||
|
Logger.debug('判断为无声音频:文本为空且时长充足'); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 如果没有utterances且响应状态正常,可能是无声音频 |
||||
|
if (!hasUtterances) { |
||||
|
// 检查是否有明确的成功状态码 |
||||
|
bool hasSuccessCode = false; |
||||
|
if (response['code'] != null) { |
||||
|
int code = response['code']; |
||||
|
hasSuccessCode = (code == 0 || code == 200); |
||||
|
} |
||||
|
|
||||
|
// 如果有成功状态码但没有utterances,很可能是无声音频 |
||||
|
if (hasSuccessCode) { |
||||
|
Logger.debug('判断为无声音频:成功状态码但无语句'); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Logger.debug('未判定为无声音频'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
Future<void> _addTextList(int id, List utterances, |
||||
|
[Map? templateData]) async { |
||||
|
Logger.debug('处理会议 $id 的 ${utterances.length} 条语句'); |
||||
|
String text = ''; |
||||
|
String personnel = ''; |
||||
|
List<Map<String, Object?>> list = []; |
||||
|
|
||||
|
for (int i = 0; i < utterances.length; i++) { |
||||
|
var element = utterances[i]; |
||||
|
Logger.debug('处理第 $i 条语句:${element.toString()}'); |
||||
|
|
||||
|
String speaker = ''; |
||||
|
String content = ''; |
||||
|
int startTime = 0; |
||||
|
int endTime = 0; |
||||
|
|
||||
|
// 提取说话人信息 - 检查多种可能的字段名 |
||||
|
if (element['additions'] != null && element['additions'].isNotEmpty) { |
||||
|
if (element['additions']['speaker'] != null) { |
||||
|
speaker = 'Speaker ${element['additions']['speaker']}'; |
||||
|
} |
||||
|
} else if (element['speaker'] != null) { |
||||
|
speaker = 'Speaker ${element['speaker']}'; |
||||
|
} else if (element['speaker_id'] != null) { |
||||
|
speaker = 'Speaker ${element['speaker_id']}'; |
||||
|
} |
||||
|
|
||||
|
// 提取文本内容 - 检查多种可能的字段名 |
||||
|
if (element['text'] != null) { |
||||
|
content = element['text'].toString(); |
||||
|
} else if (element['content'] != null) { |
||||
|
content = element['content'].toString(); |
||||
|
} else if (element['transcript'] != null) { |
||||
|
content = element['transcript'].toString(); |
||||
|
} else { |
||||
|
Logger.error('语句中未找到文本内容:$element'); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// 提取时间信息 - 检查多种可能的字段名和格式 |
||||
|
if (element['start_time'] != null) { |
||||
|
startTime = _parseTime(element['start_time']); |
||||
|
} else if (element['startTime'] != null) { |
||||
|
startTime = _parseTime(element['startTime']); |
||||
|
} else if (element['begin'] != null) { |
||||
|
startTime = _parseTime(element['begin']); |
||||
|
} |
||||
|
|
||||
|
if (element['end_time'] != null) { |
||||
|
endTime = _parseTime(element['end_time']); |
||||
|
} else if (element['endTime'] != null) { |
||||
|
endTime = _parseTime(element['endTime']); |
||||
|
} else if (element['end'] != null) { |
||||
|
endTime = _parseTime(element['end']); |
||||
|
} |
||||
|
|
||||
|
if (speaker.isNotEmpty) { |
||||
|
text += '[$speaker]: '; |
||||
|
if (!personnel.contains(speaker)) { |
||||
|
personnel += '[$speaker] '; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
list.add({ |
||||
|
'meetingid': id, |
||||
|
'content': content, |
||||
|
'starttime': startTime, |
||||
|
'endtime': endTime, |
||||
|
'speaker': speaker, |
||||
|
}); |
||||
|
text += '$content\n'; |
||||
|
|
||||
|
Logger.debug('已添加语句:说话人=$speaker,内容=$content,开始=$startTime,结束=$endTime'); |
||||
|
} |
||||
|
|
||||
|
Logger.debug('处理完成的语句总数:${list.length}'); |
||||
|
var result = await SqfliteApi.insertMeetingSpeaker(list); |
||||
|
if (result != null) { |
||||
|
// 初始用于总结/概览的文本,先用原始拼接文本作为默认值 |
||||
|
String summarySourceText = text; |
||||
|
// 根据目标语言决定是否需要翻译文本列表 |
||||
|
final String targetLanguage = |
||||
|
((templateData?['targetLanguage']) ?? '').toString(); |
||||
|
final String sourceLanguage = |
||||
|
((templateData?['language']) ?? '').toString(); |
||||
|
bool needTranslate = false; |
||||
|
if (targetLanguage.isNotEmpty) { |
||||
|
// 若未指定录音语言,默认认为需要翻译;否则当目标语言与录音语言主码不同则翻译 |
||||
|
if (sourceLanguage.isEmpty) { |
||||
|
needTranslate = true; |
||||
|
} else { |
||||
|
String srcPrefix = sourceLanguage.toLowerCase().substring(0, 2); |
||||
|
String tgtPrefix = targetLanguage.toLowerCase().substring(0, 2); |
||||
|
needTranslate = srcPrefix != tgtPrefix; |
||||
|
} |
||||
|
} |
||||
|
if (needTranslate) { |
||||
|
try { |
||||
|
// 保持在“正在转写”状态,先进行翻译为目标语言 |
||||
|
await _translateTextList(id, targetLanguage); |
||||
|
} catch (e) { |
||||
|
Logger.error('转写文本翻译失败:${e.toString()}'); |
||||
|
} |
||||
|
// 翻译完成后再写入 personnel、设置已完成并刷新 UI |
||||
|
await SqfliteApi.editMeeting( |
||||
|
id, {'tasktype': 2, 'personnel': personnel}); |
||||
|
List speakerList = await SqfliteApi.getMeetingSpeaker(id); |
||||
|
List newSpeakerList = List.from(speakerList); |
||||
|
Synchrodata.saveOperationRecord('update', { |
||||
|
'original': jsonEncode(newSpeakerList), |
||||
|
'id': id, |
||||
|
}); |
||||
|
if (id == meetingId) { |
||||
|
textList = newSpeakerList; |
||||
|
tasktype.value = 2; |
||||
|
} |
||||
|
// 由聚合器统一对外呈现状态,不直接写入标准版列表 |
||||
|
// 由聚合器统一对外呈现状态,不直接写入标准版列表 |
||||
|
// 用最新(已翻译)的文本重建总结/概览输入 |
||||
|
summarySourceText = ''; |
||||
|
for (var element in newSpeakerList) { |
||||
|
final String spk = (element['speaker'] ?? '').toString(); |
||||
|
final String cnt = (element['content'] ?? '').toString(); |
||||
|
if (spk.isNotEmpty) { |
||||
|
summarySourceText += '[$spk]: '; |
||||
|
} |
||||
|
summarySourceText += '$cnt\n'; |
||||
|
} |
||||
|
Map taskFinishData = taskFinishList.firstWhere( |
||||
|
(element) => element['id'] == id, |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
if (taskFinishData.isNotEmpty) { |
||||
|
taskFinishData['tasktype'] = 2; |
||||
|
taskFinishList.refresh(); |
||||
|
} |
||||
|
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
||||
|
} else { |
||||
|
// 原有中文直出流程 |
||||
|
await SqfliteApi.editMeeting( |
||||
|
id, {'tasktype': 2, 'personnel': personnel}); |
||||
|
List speakerList = await SqfliteApi.getMeetingSpeaker(id); |
||||
|
List newSpeakerList = List.from(speakerList); |
||||
|
Synchrodata.saveOperationRecord('update', { |
||||
|
'original': jsonEncode(newSpeakerList), |
||||
|
'id': id, |
||||
|
}); |
||||
|
if (id == meetingId) { |
||||
|
textList = newSpeakerList; |
||||
|
tasktype.value = 2; |
||||
|
} |
||||
|
// 仍使用最新DB中的文本构建总结/概览输入,避免与原始拼接文本不一致 |
||||
|
summarySourceText = ''; |
||||
|
for (var element in newSpeakerList) { |
||||
|
final String spk = (element['speaker'] ?? '').toString(); |
||||
|
final String cnt = (element['content'] ?? '').toString(); |
||||
|
if (spk.isNotEmpty) { |
||||
|
summarySourceText += '[$spk]: '; |
||||
|
} |
||||
|
summarySourceText += '$cnt\n'; |
||||
|
} |
||||
|
Map taskFinishData = taskFinishList.firstWhere( |
||||
|
(element) => element['id'] == id, |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
if (taskFinishData.isNotEmpty) { |
||||
|
taskFinishData['tasktype'] = 2; |
||||
|
taskFinishList.refresh(); |
||||
|
} |
||||
|
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
||||
|
} |
||||
|
// 使用统一的总结服务生成概览与总结 |
||||
|
final String tplPrompt = ((templateData?['prompt']) ?? '').toString(); |
||||
|
final String tplOutline = ((templateData?['outline']) ?? '').toString(); |
||||
|
final String outCode = (((templateData?['targetLanguage']) ?? |
||||
|
(templateData?['language']) ?? |
||||
|
'') as String) |
||||
|
.toLowerCase(); |
||||
|
String langDesc = ''; |
||||
|
if (outCode.startsWith('zh')) { |
||||
|
langDesc = '中文'; |
||||
|
} else if (outCode.startsWith('en')) { |
||||
|
langDesc = '英文'; |
||||
|
} else if (outCode.startsWith('ja')) { |
||||
|
langDesc = '日文'; |
||||
|
} else if (outCode.startsWith('ko')) { |
||||
|
langDesc = '韩文'; |
||||
|
} else if (outCode.startsWith('fr')) { |
||||
|
langDesc = '法文'; |
||||
|
} else if (outCode.startsWith('de')) { |
||||
|
langDesc = '德文'; |
||||
|
} else if (outCode.startsWith('es')) { |
||||
|
langDesc = '西班牙文'; |
||||
|
} else if (outCode.startsWith('ru')) { |
||||
|
langDesc = '俄文'; |
||||
|
} else if (outCode.startsWith('it')) { |
||||
|
langDesc = '意大利文'; |
||||
|
} else if (outCode.startsWith('pt')) { |
||||
|
langDesc = '葡萄牙文'; |
||||
|
} else if (outCode.startsWith('th')) { |
||||
|
langDesc = '泰文'; |
||||
|
} else if (outCode.startsWith('vi')) { |
||||
|
langDesc = '越南文'; |
||||
|
} |
||||
|
final String promptToUse = tplPrompt.isEmpty |
||||
|
? (langDesc.isEmpty |
||||
|
? '' |
||||
|
: '输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。') |
||||
|
: (langDesc.isEmpty |
||||
|
? tplPrompt |
||||
|
: tplPrompt + ' 输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。'); |
||||
|
final String outlineToUse = tplOutline.isEmpty |
||||
|
? (langDesc.isEmpty |
||||
|
? '' |
||||
|
: '输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。') |
||||
|
: (langDesc.isEmpty |
||||
|
? tplOutline |
||||
|
: tplOutline + ' 输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。'); |
||||
|
try { |
||||
|
final details = await SqfliteApi.getMeetingDetails(id); |
||||
|
final tid = (details['taskid'] ?? '') as String; |
||||
|
final transcriptBytes = utf8.encode(summarySourceText).length; |
||||
|
final svc = Get.isRegistered<UsageStatsService>() |
||||
|
? Get.find<UsageStatsService>() |
||||
|
: Get.put(UsageStatsService(), permanent: true); |
||||
|
svc.completeMeetingAsrRecord( |
||||
|
id: tid, |
||||
|
transcriptBytes: transcriptBytes, |
||||
|
completeTimestamp: DateTime.now().millisecondsSinceEpoch, |
||||
|
); |
||||
|
} catch (e) { |
||||
|
Logger.w('MeetingASRrecord', '完成用量记录失败: ' + e.toString()); |
||||
|
} |
||||
|
|
||||
|
_summaryTaskService.addTaskList( |
||||
|
id, summarySourceText, promptToUse, outlineToUse); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Future<void> _translateTextList(int id, String targetLanguage) async { |
||||
|
// 读取刚插入的文本条目 |
||||
|
List speakerList = await SqfliteApi.getMeetingSpeaker(id); |
||||
|
// 构造简洁的可翻译列表 |
||||
|
List toTranslate = speakerList |
||||
|
.map((e) => { |
||||
|
'id': e['id'], |
||||
|
'speaker': (e['speaker'] ?? '').toString(), |
||||
|
'content': (e['content'] ?? '').toString(), |
||||
|
}) |
||||
|
.toList(); |
||||
|
|
||||
|
final String userJson = jsonEncode({ |
||||
|
'targetLanguage': targetLanguage, |
||||
|
'items': toTranslate, |
||||
|
}); |
||||
|
|
||||
|
// 根据目标语言构建提示语 |
||||
|
String tgtName = targetLanguage.toLowerCase(); |
||||
|
String langDesc = '目标语言'; |
||||
|
if (tgtName.startsWith('zh')) { |
||||
|
langDesc = '中文'; |
||||
|
} else if (tgtName.startsWith('en')) { |
||||
|
langDesc = '英文'; |
||||
|
} else if (tgtName.startsWith('ja')) { |
||||
|
langDesc = '日文'; |
||||
|
} else if (tgtName.startsWith('ko')) { |
||||
|
langDesc = '韩文'; |
||||
|
} else if (tgtName.startsWith('fr')) { |
||||
|
langDesc = '法文'; |
||||
|
} else if (tgtName.startsWith('de')) { |
||||
|
langDesc = '德文'; |
||||
|
} else if (tgtName.startsWith('es')) { |
||||
|
langDesc = '西班牙文'; |
||||
|
} else if (tgtName.startsWith('ru')) { |
||||
|
langDesc = '俄文'; |
||||
|
} else if (tgtName.startsWith('it')) { |
||||
|
langDesc = '意大利文'; |
||||
|
} else if (tgtName.startsWith('pt')) { |
||||
|
langDesc = '葡萄牙文'; |
||||
|
} else if (tgtName.startsWith('th')) { |
||||
|
langDesc = '泰文'; |
||||
|
} else if (tgtName.startsWith('vi')) { |
||||
|
langDesc = '越南文'; |
||||
|
} |
||||
|
|
||||
|
final String systemPrompt = '你是专业的翻译助手。请将每条items中的content精准翻译为' + |
||||
|
langDesc + |
||||
|
',保持术语专业、语气自然。仅返回JSON数组,元素形如{"id":<原id>,"content":"<翻译后文本>"},不要额外说明,不要漏掉任何一条。'; |
||||
|
|
||||
|
final String? translated = await DeapsoundAIService().sendMessage( |
||||
|
messages: [ |
||||
|
{'role': 'system', 'content': systemPrompt}, |
||||
|
{'role': 'user', 'content': userJson}, |
||||
|
], |
||||
|
); |
||||
|
|
||||
|
if (translated == null || translated.isEmpty) return; |
||||
|
List<dynamic> arr; |
||||
|
try { |
||||
|
arr = jsonDecode(translated); |
||||
|
} catch (_) { |
||||
|
// 若模型返回包裹对象,尝试取字段 |
||||
|
try { |
||||
|
final obj = jsonDecode(translated); |
||||
|
arr = (obj is Map && obj['data'] is List) ? (obj['data'] as List) : []; |
||||
|
} catch (e) { |
||||
|
Logger.error('翻译结果 JSON 解析失败:${e.toString()}'); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
if (arr.isEmpty) return; |
||||
|
// 批量更新 |
||||
|
for (var item in arr) { |
||||
|
if (item is Map && item['id'] != null) { |
||||
|
await SqfliteApi.editMeetingSpeaker([ |
||||
|
{ |
||||
|
'id': item['id'], |
||||
|
'content': (item['content'] ?? '').toString(), |
||||
|
} |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
|
// 刷新缓存 |
||||
|
if (id == meetingId) { |
||||
|
textList = await SqfliteApi.getMeetingSpeaker(id); |
||||
|
// 主动触发监听刷新 UI(MeetingDetailsController 通过监听 tasktype 同步 textList) |
||||
|
try { |
||||
|
tasktype.refresh(); |
||||
|
} catch (_) {} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,187 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:get_storage/get_storage.dart'; |
||||
|
|
||||
|
import '../../../core/utils/logger.dart'; |
||||
|
import '../db/sqflite_api.dart'; |
||||
|
import '../deapsound_ai_service.dart'; |
||||
|
import 'meeting_task_service.dart'; |
||||
|
import 'meeting_task_service2.dart'; |
||||
|
|
||||
|
class SummaryTaskService extends GetxService { |
||||
|
final _taskService = Get.find<MeetingTaskService>(); |
||||
|
final _taskService2 = Get.find<MeetingTaskService2>(); |
||||
|
|
||||
|
final GetStorage _storage = GetStorage(); |
||||
|
|
||||
|
List _taskList = []; |
||||
|
|
||||
|
bool _isTask = false; |
||||
|
|
||||
|
String summary = ''; //AI总结内容 |
||||
|
String overview = ''; //AI概览内容 |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
_getTaskList(); |
||||
|
} |
||||
|
|
||||
|
void cancelTask() { |
||||
|
_taskList.clear(); |
||||
|
_storage.remove("summary_task_list"); |
||||
|
} |
||||
|
|
||||
|
void _getTaskList() { |
||||
|
_taskList = _storage.read("summary_task_list") ?? []; |
||||
|
_executeTask(); |
||||
|
} |
||||
|
|
||||
|
Future<void> addTaskList( |
||||
|
int id, String text, String prompt, String outline) async { |
||||
|
Logger.debug("添加总结任务列表"); |
||||
|
_taskList.add({ |
||||
|
'id': id, |
||||
|
'text': text, |
||||
|
'prompt': prompt, |
||||
|
'outline': outline, |
||||
|
}); |
||||
|
_storage.write("summary_task_list", _taskList); |
||||
|
_executeTask(); |
||||
|
} |
||||
|
|
||||
|
void _executeTask() async { |
||||
|
if (_taskList.isEmpty) return; |
||||
|
if (_isTask) return; |
||||
|
_isTask = true; |
||||
|
var task = _taskList.first; |
||||
|
_aiServiceMessage( |
||||
|
task['id'], |
||||
|
task['text'], |
||||
|
task['prompt'], |
||||
|
task['outline'], |
||||
|
); |
||||
|
Logger.debug("执行总结任务列表"); |
||||
|
} |
||||
|
|
||||
|
void removeTask(int id) { |
||||
|
_taskList.removeWhere((element) => element['id'] == id); |
||||
|
_storage.write("summary_task_list", _taskList); |
||||
|
} |
||||
|
|
||||
|
void _aiServiceMessage(int id, String text, String prompt, String outline) { |
||||
|
final ai = DeapsoundAIService(); |
||||
|
String summaryText = ''; |
||||
|
String overviewText = ''; |
||||
|
var summaryDone = false; |
||||
|
var overviewDone = false; |
||||
|
var summarySuccess = false; |
||||
|
var overviewSuccess = false; |
||||
|
|
||||
|
void tryFinish() { |
||||
|
if (summaryDone && overviewDone) { |
||||
|
if (summarySuccess && |
||||
|
overviewSuccess && |
||||
|
summaryText.isNotEmpty && |
||||
|
overviewText.isNotEmpty) { |
||||
|
_aiServiceValue(id, summaryText, overviewText); |
||||
|
} else { |
||||
|
_isTask = false; |
||||
|
_executeTask(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
final summaryStream = ai.sendMessageStream(messages: [ |
||||
|
{'role': 'system', 'content': prompt}, |
||||
|
{'role': 'user', 'content': text}, |
||||
|
]).timeout(const Duration(seconds: 120)); |
||||
|
|
||||
|
final overviewStream = ai.sendMessageStream(messages: [ |
||||
|
{'role': 'system', 'content': outline}, |
||||
|
{'role': 'user', 'content': text}, |
||||
|
]).timeout(const Duration(seconds: 120)); |
||||
|
|
||||
|
summaryStream.listen((chunk) { |
||||
|
summaryText += chunk; |
||||
|
}, onError: (error) { |
||||
|
summaryDone = true; |
||||
|
summarySuccess = false; |
||||
|
tryFinish(); |
||||
|
}, onDone: () { |
||||
|
summaryDone = true; |
||||
|
summarySuccess = true; |
||||
|
tryFinish(); |
||||
|
}, cancelOnError: true); |
||||
|
|
||||
|
overviewStream.listen((chunk) { |
||||
|
overviewText += chunk; |
||||
|
}, onError: (error) { |
||||
|
overviewDone = true; |
||||
|
overviewSuccess = false; |
||||
|
tryFinish(); |
||||
|
}, onDone: () { |
||||
|
overviewDone = true; |
||||
|
overviewSuccess = true; |
||||
|
tryFinish(); |
||||
|
}, cancelOnError: true); |
||||
|
} |
||||
|
|
||||
|
Future<void> _aiServiceValue( |
||||
|
int id, String summaryText, String overviewText) async { |
||||
|
final exists = _taskList.any((item) => item['id'] == id); |
||||
|
if (!exists) { |
||||
|
_isTask = false; |
||||
|
_executeTask(); |
||||
|
return; |
||||
|
} |
||||
|
await SqfliteApi.editMeeting(id, { |
||||
|
'tasktype': 3, |
||||
|
'summary': summaryText, |
||||
|
'overview': overviewText, |
||||
|
}); |
||||
|
|
||||
|
// 更新 V1 Service 状态 |
||||
|
if (id == _taskService.meetingId) { |
||||
|
summary = summaryText; |
||||
|
overview = overviewText; |
||||
|
_taskService.tasktype.value = 3; |
||||
|
} |
||||
|
|
||||
|
// 更新 V2 Service 状态 |
||||
|
if (id == _taskService2.meetingId) { |
||||
|
summary = summaryText; |
||||
|
overview = overviewText; |
||||
|
_taskService2.summary = summaryText; |
||||
|
_taskService2.overview = overviewText; |
||||
|
_taskService2.tasktype.value = 3; |
||||
|
} |
||||
|
|
||||
|
// 更新 V1 任务完成列表 |
||||
|
Map taskFinishData = _taskService.taskFinishList.firstWhere( |
||||
|
(element) => element['id'] == id, |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
if (taskFinishData.isNotEmpty) { |
||||
|
taskFinishData['tasktype'] = 3; |
||||
|
_taskService.taskFinishList.refresh(); |
||||
|
} |
||||
|
_storage.write("meeting_task_finish_list", _taskService.taskFinishList); |
||||
|
|
||||
|
// 更新 V2 任务完成列表 |
||||
|
Map taskFinishData2 = _taskService2.taskFinishList.firstWhere( |
||||
|
(element) => element['id'] == id, |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
if (taskFinishData2.isNotEmpty) { |
||||
|
taskFinishData2['tasktype'] = 3; |
||||
|
_taskService2.taskFinishList.refresh(); |
||||
|
} |
||||
|
_storage.write("meeting_task_finish_list_v2", _taskService2.taskFinishList); |
||||
|
|
||||
|
_taskList.removeWhere((element) => element['id'] == id); |
||||
|
_storage.write("summary_task_list", _taskList); |
||||
|
_isTask = false; |
||||
|
_executeTask(); |
||||
|
Logger.debug("总结任务完成"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,783 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:get_storage/get_storage.dart'; |
||||
|
import '../models/translation_usage_stats.dart'; |
||||
|
import '../models/meeting_usage_stats.dart'; |
||||
|
import '../../core/utils/logger.dart'; |
||||
|
import 'network/api.dart'; |
||||
|
import '../models/goods.dart'; |
||||
|
import '../models/user_Info.dart'; |
||||
|
|
||||
|
/// 使用量统计服务 |
||||
|
/// |
||||
|
class UsageStatsService extends GetxService { |
||||
|
static const String _dailyStatsKey = 'daily_translation_stats'; |
||||
|
static const String _meetingDailyKey = 'daily_meeting_asr_stats'; |
||||
|
static const String _meetingOsDailykey = 'daily_meeting_os_stats'; |
||||
|
|
||||
|
final GetStorage _storage = GetStorage(); |
||||
|
|
||||
|
// 实时统计数据 |
||||
|
final Rx<RealtimeUsageStats> realtimeTranslationStats = |
||||
|
RealtimeUsageStats().obs; |
||||
|
|
||||
|
// 当日统计数据 |
||||
|
final Rx<TranslationUsageStats> todayTranslationStats = |
||||
|
TranslationUsageStats.empty('').obs; |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
_initializeService(); |
||||
|
} |
||||
|
|
||||
|
/// 初始化服务 |
||||
|
void _initializeService() { |
||||
|
_loadTodayTranslationStats(); |
||||
|
Logger.info('lxm--使用量统计服务初始化完成'); |
||||
|
} |
||||
|
|
||||
|
/// 加载当日翻译统计数据 |
||||
|
void _loadTodayTranslationStats() { |
||||
|
try { |
||||
|
final today = DateTime.now(); |
||||
|
final todayKey = |
||||
|
'${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; |
||||
|
|
||||
|
final statsData = _storage.read('${_dailyStatsKey}_$todayKey'); |
||||
|
if (statsData != null) { |
||||
|
todayTranslationStats.value = TranslationUsageStats.fromJson( |
||||
|
Map<String, dynamic>.from(statsData)); |
||||
|
} else { |
||||
|
todayTranslationStats.value = |
||||
|
TranslationUsageStats.empty(getCurrentUserId()); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.error('加载当日翻译统计数据失败: $e'); |
||||
|
todayTranslationStats.value = |
||||
|
TranslationUsageStats.empty(getCurrentUserId()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取当前用户ID |
||||
|
String getCurrentUserId() { |
||||
|
try { |
||||
|
final userInfo = _storage.read('user_info'); |
||||
|
if (userInfo != null && userInfo['user'] != null) { |
||||
|
return userInfo['user']['uid'] ?? 'anonymous'; |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.warning('获取用户ID失败: $e'); |
||||
|
} |
||||
|
return 'anonymous'; |
||||
|
} |
||||
|
|
||||
|
MeetingAsrDailyStats _loadMeetingDaily(String dateKey) { |
||||
|
final data = _storage.read('${_meetingDailyKey}_$dateKey'); |
||||
|
if (data != null) { |
||||
|
return MeetingAsrDailyStats.fromJson(Map<String, dynamic>.from(data)); |
||||
|
} |
||||
|
return MeetingAsrDailyStats( |
||||
|
date: dateKey, userId: getCurrentUserId(), items: []); |
||||
|
} |
||||
|
|
||||
|
void _saveMeetingDaily(MeetingAsrDailyStats stats) { |
||||
|
_storage.write('${_meetingDailyKey}_${stats.date}', stats.toJson()); |
||||
|
Logger.d('MeetingASRrecord', |
||||
|
'保存会议当日统计: date=${stats.date}, items=${stats.items.length}'); |
||||
|
} |
||||
|
|
||||
|
String _formatDate(DateTime d) { |
||||
|
final y = d.year; |
||||
|
final m = d.month.toString().padLeft(2, '0'); |
||||
|
final day = d.day.toString().padLeft(2, '0'); |
||||
|
return '$y-$m-$day'; |
||||
|
} |
||||
|
|
||||
|
/// 记录单次对象存储上传 |
||||
|
/// [meetingId] 会议ID |
||||
|
/// [storageUrl] 云端存储URL |
||||
|
/// [storageBytes] 文件大小(字节) |
||||
|
/// [transferBytes] 数据传输字节数 |
||||
|
/// [uploadTimestamp] 上传完成时间戳(毫秒) |
||||
|
void recordMeetingObjectStorage({ |
||||
|
required int meetingId, |
||||
|
required String storageUrl, |
||||
|
required int storageBytes, |
||||
|
required int transferBytes, |
||||
|
required int uploadTimestamp, |
||||
|
}) { |
||||
|
final dateKey = |
||||
|
_formatDate(DateTime.fromMillisecondsSinceEpoch(uploadTimestamp)); |
||||
|
final storageKey = '${_meetingOsDailykey}_$dateKey'; |
||||
|
|
||||
|
final raw = _storage.read(storageKey); |
||||
|
List<MeetingObjectStorageRecord> uploads = []; |
||||
|
if (raw is List) { |
||||
|
uploads = raw |
||||
|
.map((e) => |
||||
|
MeetingObjectStorageRecord.fromJson(Map<String, dynamic>.from(e))) |
||||
|
.toList(); |
||||
|
} |
||||
|
|
||||
|
// final id = 'os_${uploadTimestamp}_${meetingId}'; |
||||
|
final record = MeetingObjectStorageRecord( |
||||
|
meetingId: meetingId, |
||||
|
storageUrl: storageUrl, |
||||
|
storageBytes: storageBytes, |
||||
|
transferBytes: transferBytes, |
||||
|
uploadTimestamp: uploadTimestamp, |
||||
|
); |
||||
|
|
||||
|
// 使用 meetingId + storageUrl 作为唯一标识来判断重复 |
||||
|
final exists = uploads.indexWhere( |
||||
|
(e) => e.meetingId == meetingId && e.storageUrl == storageUrl) >= |
||||
|
0; |
||||
|
if (!exists) { |
||||
|
uploads.add(record); |
||||
|
_storage.write(storageKey, uploads.map((e) => e.toJson()).toList()); |
||||
|
Logger.d('MeetingOSrecord', |
||||
|
'新增对象存储上传: meetingId=$meetingId, size=$storageBytes, transfer=$transferBytes'); |
||||
|
} else { |
||||
|
Logger.d('MeetingOSrecord', |
||||
|
'重复对象存储上传忽略: meetingId=$meetingId, url=$storageUrl'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 添加或更新会议ASR记录 |
||||
|
/// [record] 会议ASR记录 |
||||
|
/// 该方法用于记录一次会议语音识别(ASR)的提交记录。它会根据记录的时间戳确定日期, |
||||
|
/// 并将记录添加到该日期的会议ASR记录列表中。如果记录已存在,则会更新该记录。 |
||||
|
void addMeetingAsrRecord(MeetingAsrRecord record) { |
||||
|
final dateKey = _formatDate( |
||||
|
DateTime.fromMillisecondsSinceEpoch(record.submitTimestamp)); |
||||
|
final daily = _loadMeetingDaily(dateKey); |
||||
|
final idx = daily.items.indexWhere((e) => e.id == record.id); |
||||
|
if (idx >= 0) { |
||||
|
daily.items[idx] = record; |
||||
|
Logger.d('MeetingASRrecord', |
||||
|
'更新会议ASR记录: id=${record.id}, meetingId=${record.meetingId}, date=${dateKey}'); |
||||
|
} else { |
||||
|
daily.items.add(record); |
||||
|
Logger.d('MeetingASRrecord', |
||||
|
'新增会议ASR记录: id=${record.id}, meetingId=${record.meetingId}, date=${dateKey}'); |
||||
|
} |
||||
|
_saveMeetingDaily(daily); |
||||
|
Logger.i('MeetingASRrecord', |
||||
|
'会议ASR提交: id=${record.id}, meetingId=${record.meetingId}, audioSeconds=${record.audioSeconds}, fileSizeBytes=${record.fileSizeBytes}, language=${record.language}, date=${dateKey}'); |
||||
|
} |
||||
|
|
||||
|
/// 完成会议ASR记录 |
||||
|
/// [id] 会议ASR记录ID |
||||
|
/// [transcriptBytes] 转写结果文本字节数 |
||||
|
/// [completeTimestamp] 完成时间戳 |
||||
|
/// 该方法用于标记一次会议语音识别(ASR)任务为已完成状态。 |
||||
|
/// 它会根据记录ID在最近3天的会议ASR记录列表中查找匹配项, |
||||
|
/// 如果找到,则更新该记录的状态为已完成,并记录转写结果文本字节数和完成时间戳。 |
||||
|
void completeMeetingAsrRecord({ |
||||
|
required String id, |
||||
|
required int transcriptBytes, |
||||
|
required int completeTimestamp, |
||||
|
}) { |
||||
|
final now = DateTime.now(); |
||||
|
for (int i = 0; i < 3; i++) { |
||||
|
final dateKey = _formatDate(now.subtract(Duration(days: i))); |
||||
|
final daily = _loadMeetingDaily(dateKey); |
||||
|
final idx = daily.items.indexWhere((e) => e.id == id); |
||||
|
if (idx >= 0) { |
||||
|
final old = daily.items[idx]; |
||||
|
daily.items[idx] = MeetingAsrRecord( |
||||
|
id: old.id, |
||||
|
meetingId: old.meetingId, |
||||
|
taskId: old.taskId, |
||||
|
audioSeconds: old.audioSeconds, |
||||
|
fileSizeBytes: old.fileSizeBytes, |
||||
|
language: old.language, |
||||
|
submitTimestamp: old.submitTimestamp, |
||||
|
status: asrStatusCompleted, |
||||
|
transcriptBytes: transcriptBytes, |
||||
|
completeTimestamp: completeTimestamp, |
||||
|
); |
||||
|
_saveMeetingDaily(daily); |
||||
|
Logger.i('MeetingASRrecord', |
||||
|
'会议ASR完成: id=${id}, meetingId=${old.meetingId}, transcriptBytes=${transcriptBytes}, audioSeconds=${old.audioSeconds}, date=${dateKey}'); |
||||
|
try { |
||||
|
_reportMeetingUsageToServer( |
||||
|
audioSeconds: old.audioSeconds, |
||||
|
audioBytes: old.fileSizeBytes, |
||||
|
language: old.language, |
||||
|
transcriptBytes: transcriptBytes, |
||||
|
); |
||||
|
} catch (e) { |
||||
|
Logger.error('会议用量上报(完成)失败: $e'); |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 上报会议用量到服务器 |
||||
|
/// [audioSeconds] 音频时长(秒) |
||||
|
/// [audioBytes] 音频文件大小(字节) |
||||
|
/// [language] 识别语言 |
||||
|
/// [transcriptBytes] 转写结果文本字节数(可选) |
||||
|
/// 该方法用于将会议语音识别(ASR)的用量数据上报到服务器。 |
||||
|
/// 它会根据提供的参数构建一个包含会议用量类型、音频时长、音频文件大小、 |
||||
|
/// 识别语言以及可选的转写结果文本字节数的参数对象。 |
||||
|
/// 然后,它会调用API接口将该参数对象发送到服务器进行处理。 |
||||
|
/// 如果上报成功,服务器会返回一个响应,该方法会根据响应更新本地的用户积分(非总积分)。 |
||||
|
Future<void> _reportMeetingUsageToServer({ |
||||
|
required int audioSeconds, |
||||
|
required int audioBytes, |
||||
|
required String language, |
||||
|
int? transcriptBytes, |
||||
|
}) async { |
||||
|
try { |
||||
|
final params = { |
||||
|
'usagetype': UsageType.Meet.toInt, |
||||
|
'usages': { |
||||
|
'MEETING_ASR_AUDIO_SECONDS': audioSeconds, |
||||
|
// 'MEETING_ASR_AUDIO_BYTES': audioBytes, |
||||
|
// if (transcriptBytes != null) |
||||
|
// 'MEETING_ASR_TRANSCRIPT_BYTES': transcriptBytes, |
||||
|
// 'MEETING_ASR_LANGUAGE_$language': 1, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
Logger.info('lxm---MEET用量上报参数: $params'); |
||||
|
|
||||
|
final response = await Api.usagesResPoints(params); |
||||
|
|
||||
|
Logger.info('lxm---MEET用量上报响应: $response'); |
||||
|
|
||||
|
_applyUsageResponse(response); |
||||
|
} catch (e) { |
||||
|
Logger.error('会议用量上报失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 记录翻译API调用 |
||||
|
void recordTranslationApiCall({ |
||||
|
required String sourceText, |
||||
|
required String targetText, |
||||
|
required String sourceLanguage, |
||||
|
required String targetLanguage, |
||||
|
required String mode, |
||||
|
}) { |
||||
|
try { |
||||
|
// 更新实时统计 |
||||
|
realtimeTranslationStats.value.translationCount++; |
||||
|
realtimeTranslationStats.value.sourceTextCharacters += sourceText.length; |
||||
|
realtimeTranslationStats.value.ttsTextCharacters += targetText.length; |
||||
|
realtimeTranslationStats.value.currentLanguagePair = |
||||
|
'$sourceLanguage-$targetLanguage'; |
||||
|
realtimeTranslationStats.value.currentMode = mode; |
||||
|
realtimeTranslationStats.refresh(); |
||||
|
|
||||
|
// 更新当日统计 - 使用多维度统计 |
||||
|
final languagePair = '$sourceLanguage→$targetLanguage'; |
||||
|
|
||||
|
// 更新语言对统计 |
||||
|
final newLanguagePairStats = Map<String, LanguagePairStats>.from( |
||||
|
todayTranslationStats.value.languagePairStats); |
||||
|
if (newLanguagePairStats.containsKey(languagePair)) { |
||||
|
final existing = newLanguagePairStats[languagePair]!; |
||||
|
newLanguagePairStats[languagePair] = LanguagePairStats( |
||||
|
languagePair: languagePair, |
||||
|
translationCount: existing.translationCount + 1, |
||||
|
sourceCharacters: existing.sourceCharacters + sourceText.length, |
||||
|
); |
||||
|
} else { |
||||
|
newLanguagePairStats[languagePair] = LanguagePairStats( |
||||
|
languagePair: languagePair, |
||||
|
translationCount: 1, |
||||
|
sourceCharacters: sourceText.length, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 更新模式统计 |
||||
|
final newModeStats = Map<String, TranslationModeStats>.from( |
||||
|
todayTranslationStats.value.modeStats); |
||||
|
if (newModeStats.containsKey(mode)) { |
||||
|
final existing = newModeStats[mode]!; |
||||
|
newModeStats[mode] = existing.copyWith( |
||||
|
apiCalls: existing.apiCalls + 1, |
||||
|
sourceCharacters: existing.sourceCharacters + sourceText.length, |
||||
|
ttsCharacters: existing.ttsCharacters + targetText.length, |
||||
|
); |
||||
|
} else { |
||||
|
newModeStats[mode] = TranslationModeStats( |
||||
|
apiCalls: 1, |
||||
|
sourceCharacters: sourceText.length, |
||||
|
ttsCharacters: targetText.length, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
todayTranslationStats.value = todayTranslationStats.value.copyWith( |
||||
|
translationApiCalls: |
||||
|
todayTranslationStats.value.translationApiCalls + 1, |
||||
|
sourceTextCharacters: todayTranslationStats.value.sourceTextCharacters + |
||||
|
sourceText.length, |
||||
|
ttsTextCharacters: |
||||
|
todayTranslationStats.value.ttsTextCharacters + targetText.length, |
||||
|
languagePairStats: newLanguagePairStats, |
||||
|
modeStats: newModeStats, |
||||
|
); |
||||
|
|
||||
|
_saveTodayStats(); |
||||
|
|
||||
|
Logger.info( |
||||
|
'记录翻译API调用: 源文本${sourceText.length}字符, 目标文本${targetText.length}字符'); |
||||
|
} catch (e) { |
||||
|
Logger.error('记录翻译API调用失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 记录ASR API调用 - 优化版(精确统计参数) |
||||
|
void recordAsrApiCall({ |
||||
|
required int effectiveDurationSeconds, // 有效语音时长(VAD检测的实际语音) |
||||
|
required int totalDurationSeconds, // 总会话时长(包含静音) |
||||
|
required String language, |
||||
|
required String mode, // 翻译模式 |
||||
|
String? audioFormat, // 音频格式(可选) |
||||
|
int? sampleRate, // 采样率(可选) |
||||
|
}) { |
||||
|
try { |
||||
|
// 更新实时统计(使用有效语音时长作为主要计费依据) |
||||
|
realtimeTranslationStats.value.effectiveAudioDuration += |
||||
|
effectiveDurationSeconds; |
||||
|
realtimeTranslationStats.value.totalSessionDuration += |
||||
|
totalDurationSeconds; |
||||
|
realtimeTranslationStats.refresh(); |
||||
|
|
||||
|
// 更新模式统计 |
||||
|
final newModeStats = Map<String, TranslationModeStats>.from( |
||||
|
todayTranslationStats.value.modeStats); |
||||
|
if (newModeStats.containsKey(mode)) { |
||||
|
final existing = newModeStats[mode]!; |
||||
|
newModeStats[mode] = existing.copyWith( |
||||
|
effectiveAudioDuration: |
||||
|
existing.effectiveAudioDuration + effectiveDurationSeconds, |
||||
|
); |
||||
|
} else { |
||||
|
newModeStats[mode] = TranslationModeStats( |
||||
|
effectiveAudioDuration: effectiveDurationSeconds, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 更新当日统计 |
||||
|
todayTranslationStats.value = todayTranslationStats.value.copyWith( |
||||
|
asrApiCalls: todayTranslationStats.value.asrApiCalls + 1, |
||||
|
effectiveAudioDuration: |
||||
|
todayTranslationStats.value.effectiveAudioDuration + |
||||
|
effectiveDurationSeconds, |
||||
|
totalSessionDuration: todayTranslationStats.value.totalSessionDuration + |
||||
|
totalDurationSeconds, |
||||
|
modeStats: newModeStats, |
||||
|
); |
||||
|
|
||||
|
_saveTodayStats(); |
||||
|
|
||||
|
Logger.info( |
||||
|
'ASR统计: 有效语音${effectiveDurationSeconds}秒, 总时长${totalDurationSeconds}秒, 语言$language, 模式$mode'); |
||||
|
} catch (e) { |
||||
|
Logger.error('记录ASR API调用失败: $e'); |
||||
|
} |
||||
|
|
||||
|
_reportAsrUsageToServer( |
||||
|
effectiveDurationSeconds: effectiveDurationSeconds, |
||||
|
//totalDurationSeconds: totalDurationSeconds, |
||||
|
language: language, |
||||
|
mode: mode, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/// 上报ASR用量到服务器 |
||||
|
/// [effectiveDurationSeconds] 有效语音时长(秒) |
||||
|
/// [language] 识别语言 |
||||
|
/// [mode] 翻译模式 |
||||
|
/// 该方法用于将ASR(语音识别)的用量数据上报到服务器。 |
||||
|
/// 它会根据提供的参数构建一个包含用量类型、有效语音时长、识别语言以及翻译模式的参数对象。 |
||||
|
/// 然后,它会调用API接口将该参数对象发送到服务器进行处理。 |
||||
|
/// 如果上报成功,服务器会返回一个响应,该方法会根据响应更新本地的用户积分(非总积分)。 |
||||
|
Future<void> _reportAsrUsageToServer({ |
||||
|
required int effectiveDurationSeconds, |
||||
|
//required int totalDurationSeconds, |
||||
|
required String language, |
||||
|
required String mode, |
||||
|
}) async { |
||||
|
try { |
||||
|
final params = { |
||||
|
'usagetype': UsageType.Translation.toInt, |
||||
|
'usages': { |
||||
|
// 'TRANSLATION_ASR_EFFECTIVE_SECONDS': effectiveDurationSeconds, |
||||
|
//'TRANSLATION_ASR_TOTAL_SECONDS': totalDurationSeconds, |
||||
|
'TRANSLATION_MODE_$mode': effectiveDurationSeconds, |
||||
|
'TRANSLATION_ASR_LANGUAGE_$language': 1, |
||||
|
// 'TRANSLATION_SOURCE_CHARACTERS': |
||||
|
// realtimeTranslationStats.value.sourceTextCharacters, |
||||
|
// 'TRANSLATION_TARGET_CHARACTERS': |
||||
|
// realtimeTranslationStats.value.ttsTextCharacters, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
Logger.info('lxm---ASR用量上报参数: $params'); |
||||
|
|
||||
|
final response = await Api.usagesResPoints(params); |
||||
|
|
||||
|
Logger.info('lxm---ASR用量上报响应: $response'); |
||||
|
|
||||
|
_applyUsageResponse(response); |
||||
|
} catch (e) { |
||||
|
Logger.error('记录ASR API调用失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 根据服务器用量上报回应,更新用户积分(非总积分) |
||||
|
/// 用量类型:0 AI聊天、1 翻译、2 会议 |
||||
|
void _applyUsageResponse(dynamic response) { |
||||
|
try { |
||||
|
if (response is! Map) return; |
||||
|
final usagesObj = response['usages']; |
||||
|
if (usagesObj is! Map) return; |
||||
|
final usages = Map<String, dynamic>.from(usagesObj as Map); |
||||
|
int _asInt(dynamic v) { |
||||
|
if (v == null) return 0; |
||||
|
if (v is int) return v; |
||||
|
if (v is num) return v.toInt(); |
||||
|
if (v is String) return int.tryParse(v) ?? 0; |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
final Map<String, dynamic> update = {}; |
||||
|
usages.forEach((k, v) { |
||||
|
final key = int.tryParse(k.toString()); |
||||
|
final val = _asInt(v); |
||||
|
if (key == null) return; |
||||
|
switch (key) { |
||||
|
case 0: |
||||
|
update['aichatintegral'] = val; |
||||
|
break; |
||||
|
case 1: |
||||
|
update['tradeintegral'] = val; |
||||
|
break; |
||||
|
case 2: |
||||
|
update['meetintegral'] = val; |
||||
|
break; |
||||
|
} |
||||
|
}); |
||||
|
if (update.isNotEmpty) { |
||||
|
User.instance.updateUserInfo(update); |
||||
|
Logger.info('积分更新: $update'); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.error('解析用量回应并更新积分失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 记录TTS API调用 |
||||
|
void recordTtsApiCall({ |
||||
|
required String text, |
||||
|
required int durationSeconds, |
||||
|
required String language, |
||||
|
}) { |
||||
|
try { |
||||
|
// 更新实时统计 |
||||
|
realtimeTranslationStats.value.totalSessionDuration += durationSeconds; |
||||
|
realtimeTranslationStats.refresh(); |
||||
|
|
||||
|
// 更新当日统计(TTS按字符数计费,不是按时长) |
||||
|
todayTranslationStats.value = todayTranslationStats.value.copyWith( |
||||
|
ttsApiCalls: todayTranslationStats.value.ttsApiCalls + 1, |
||||
|
ttsTextCharacters: |
||||
|
todayTranslationStats.value.ttsTextCharacters + text.length, |
||||
|
totalSessionDuration: |
||||
|
todayTranslationStats.value.totalSessionDuration + durationSeconds, |
||||
|
); |
||||
|
|
||||
|
_saveTodayStats(); |
||||
|
|
||||
|
Logger.info('记录TTS API调用: 文本${text.length}字符, 时长${durationSeconds}秒'); |
||||
|
} catch (e) { |
||||
|
Logger.error('记录TTS API调用失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 记录AI摘要API调用 |
||||
|
void recordAiSummaryApiCall() { |
||||
|
try { |
||||
|
// 更新当日统计 |
||||
|
todayTranslationStats.value = todayTranslationStats.value.copyWith( |
||||
|
aiSummaryApiCalls: todayTranslationStats.value.aiSummaryApiCalls + 1, |
||||
|
); |
||||
|
|
||||
|
_saveTodayStats(); |
||||
|
|
||||
|
Logger.info('记录AI摘要API调用'); |
||||
|
} catch (e) { |
||||
|
Logger.error('记录AI摘要API调用失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 开始新会话 |
||||
|
void startNewSession() { |
||||
|
realtimeTranslationStats.value.reset(); |
||||
|
|
||||
|
// 更新当日会话数 |
||||
|
todayTranslationStats.value = todayTranslationStats.value.copyWith( |
||||
|
sessionCount: todayTranslationStats.value.sessionCount + 1, |
||||
|
); |
||||
|
|
||||
|
_saveTodayStats(); |
||||
|
Logger.info('开始新的翻译会话'); |
||||
|
} |
||||
|
|
||||
|
/// 保存当日统计数据 |
||||
|
void _saveTodayStats() { |
||||
|
try { |
||||
|
final today = DateTime.now(); |
||||
|
final todayKey = |
||||
|
'${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; |
||||
|
_storage.write( |
||||
|
'${_dailyStatsKey}_$todayKey', todayTranslationStats.value.toJson()); |
||||
|
} catch (e) { |
||||
|
Logger.error('保存当日统计数据失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取历史统计数据 |
||||
|
Future<List<TranslationUsageStats>> getHistoryStats({ |
||||
|
required DateTime startDate, |
||||
|
required DateTime endDate, |
||||
|
}) async { |
||||
|
try { |
||||
|
final List<TranslationUsageStats> historyStats = []; |
||||
|
|
||||
|
for (DateTime date = startDate; |
||||
|
date.isBefore(endDate) || date.isAtSameMomentAs(endDate); |
||||
|
date = date.add(const Duration(days: 1))) { |
||||
|
final dateKey = |
||||
|
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; |
||||
|
final statsData = _storage.read('${_dailyStatsKey}_$dateKey'); |
||||
|
|
||||
|
if (statsData != null) { |
||||
|
historyStats.add(TranslationUsageStats.fromJson( |
||||
|
Map<String, dynamic>.from(statsData))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return historyStats; |
||||
|
} catch (e) { |
||||
|
Logger.error('获取历史统计数据失败: $e'); |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 清理过期数据(保留最近90天) |
||||
|
void cleanupOldData() { |
||||
|
try { |
||||
|
final cutoffDate = DateTime.now().subtract(const Duration(days: 90)); |
||||
|
final allKeys = _storage.getKeys(); |
||||
|
|
||||
|
for (final key in allKeys) { |
||||
|
if (key.startsWith(_dailyStatsKey)) { |
||||
|
final dateStr = key.replaceFirst('${_dailyStatsKey}_', ''); |
||||
|
try { |
||||
|
final date = DateTime.parse(dateStr); |
||||
|
if (date.isBefore(cutoffDate)) { |
||||
|
_storage.remove(key); |
||||
|
Logger.info('清理过期统计数据: $key'); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
// 忽略解析错误的键 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.error('清理过期数据失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取月度统计汇总 |
||||
|
Future<TranslationUsageStats> getMonthlyStats(int year, int month) async { |
||||
|
try { |
||||
|
final startDate = DateTime(year, month, 1); |
||||
|
final endDate = DateTime(year, month + 1, 0); // 月末 |
||||
|
|
||||
|
final dailyStats = await getHistoryStats( |
||||
|
startDate: startDate, |
||||
|
endDate: endDate, |
||||
|
); |
||||
|
|
||||
|
if (dailyStats.isEmpty) { |
||||
|
return TranslationUsageStats.empty(getCurrentUserId()); |
||||
|
} |
||||
|
|
||||
|
// 汇总月度数据 |
||||
|
int totalTranslationCalls = 0; |
||||
|
int totalAsrCalls = 0; |
||||
|
int totalTtsCalls = 0; |
||||
|
int totalAiSummaryCalls = 0; |
||||
|
int totalSourceChars = 0; |
||||
|
int totalTargetChars = 0; |
||||
|
int totalRecordingDuration = 0; |
||||
|
int totalTtsPlayDuration = 0; |
||||
|
int totalSessions = 0; |
||||
|
//double totalCost = 0.0; |
||||
|
|
||||
|
final Map<String, int> combinedLanguagePairUsage = {}; |
||||
|
final Map<String, int> combinedModeUsage = {}; |
||||
|
|
||||
|
for (final stats in dailyStats) { |
||||
|
totalTranslationCalls += stats.translationApiCalls; |
||||
|
totalAsrCalls += stats.asrApiCalls; |
||||
|
totalTtsCalls += stats.ttsApiCalls; |
||||
|
totalAiSummaryCalls += stats.aiSummaryApiCalls; |
||||
|
totalSourceChars += stats.sourceCharacterCount; |
||||
|
totalTargetChars += stats.targetCharacterCount; |
||||
|
totalRecordingDuration += stats.audioRecordingDuration; |
||||
|
totalTtsPlayDuration += stats.ttsPlaybackDuration; |
||||
|
totalSessions += stats.sessionCount; |
||||
|
// totalCost += stats.estimatedCost; |
||||
|
|
||||
|
// 合并语言对使用统计 |
||||
|
stats.languagePairUsage.forEach((key, value) { |
||||
|
combinedLanguagePairUsage[key] = |
||||
|
(combinedLanguagePairUsage[key] ?? 0) + value; |
||||
|
}); |
||||
|
|
||||
|
// 合并模式使用统计 |
||||
|
stats.modeUsage.forEach((key, value) { |
||||
|
combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return TranslationUsageStats( |
||||
|
id: 'monthly_${year}_$month', |
||||
|
date: startDate, |
||||
|
userId: getCurrentUserId(), |
||||
|
timeGranularity: 'month', |
||||
|
translationApiCalls: totalTranslationCalls, |
||||
|
asrApiCalls: totalAsrCalls, |
||||
|
ttsApiCalls: totalTtsCalls, |
||||
|
aiSummaryApiCalls: totalAiSummaryCalls, |
||||
|
sourceTextCharacters: totalSourceChars, |
||||
|
effectiveAudioDuration: totalRecordingDuration, |
||||
|
ttsTextCharacters: totalTargetChars, |
||||
|
totalSessionDuration: totalTtsPlayDuration, |
||||
|
sessionCount: totalSessions, |
||||
|
// estimatedCost: totalCost, |
||||
|
); |
||||
|
} catch (e) { |
||||
|
Logger.error('获取月度统计失败: $e'); |
||||
|
return TranslationUsageStats.empty(getCurrentUserId()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取会议历史统计 |
||||
|
/// |
||||
|
/// 从[startDate]到[endDate]的会议ASR记录。 |
||||
|
Future<List<MeetingAsrDailyStats>> getMeetingHistoryStats({ |
||||
|
required DateTime startDate, |
||||
|
required DateTime endDate, |
||||
|
}) async { |
||||
|
try { |
||||
|
final List<MeetingAsrDailyStats> list = []; |
||||
|
for (DateTime date = startDate; |
||||
|
date.isBefore(endDate) || date.isAtSameMomentAs(endDate); |
||||
|
date = date.add(const Duration(days: 1))) { |
||||
|
final key = _formatDate(date); |
||||
|
final daily = _loadMeetingDaily(key); |
||||
|
if (daily.items.isNotEmpty) { |
||||
|
list.add(daily); |
||||
|
} |
||||
|
} |
||||
|
Logger.i('MeetingASRrecord', |
||||
|
'获取会议历史统计: start=${_formatDate(startDate)}, end=${_formatDate(endDate)}, 有效天数=${list.length}'); |
||||
|
return list; |
||||
|
} catch (e) { |
||||
|
Logger.error('获取会议历史统计失败: $e'); |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 聚合会议统计 |
||||
|
/// |
||||
|
/// 从[daily]中聚合会议统计数据。 |
||||
|
MeetingAsrHistoryStats aggregateMeetingStats( |
||||
|
List<MeetingAsrDailyStats> daily) { |
||||
|
int calls = 0; |
||||
|
int audioSeconds = 0; |
||||
|
int audioBytes = 0; |
||||
|
int transcriptBytes = 0; |
||||
|
for (final d in daily) { |
||||
|
calls += d.asrCalls; |
||||
|
audioSeconds += d.audioSecondsTotal; |
||||
|
audioBytes += d.audioBytesTotal; |
||||
|
transcriptBytes += d.transcriptBytesTotal; |
||||
|
} |
||||
|
Logger.i('MeetingASRrecord', |
||||
|
'聚合会议统计: calls=${calls}, audioSeconds=${audioSeconds}, audioBytes=${audioBytes}, transcriptBytes=${transcriptBytes}'); |
||||
|
return MeetingAsrHistoryStats( |
||||
|
userId: getCurrentUserId(), |
||||
|
allAsrCalls: calls, |
||||
|
allAudioSeconds: audioSeconds, |
||||
|
allAudioFileBytes: audioBytes, |
||||
|
allTranscriptBytes: transcriptBytes, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/// 获取会议对象存储历史统计 |
||||
|
/// |
||||
|
/// 从[startDate]到[endDate]的会议对象存储上传记录。 |
||||
|
Future<List<MeetingStorageDailyStats>> getMeetingStorageHistoryStats({ |
||||
|
required DateTime startDate, |
||||
|
required DateTime endDate, |
||||
|
}) async { |
||||
|
try { |
||||
|
final List<MeetingStorageDailyStats> list = []; |
||||
|
for (DateTime date = startDate; |
||||
|
date.isBefore(endDate) || date.isAtSameMomentAs(endDate); |
||||
|
date = date.add(const Duration(days: 1))) { |
||||
|
final key = _formatDate(date); |
||||
|
final raw = _storage.read('${_meetingOsDailykey}_$key'); |
||||
|
List<MeetingObjectStorageRecord> uploads = []; |
||||
|
if (raw is List) { |
||||
|
uploads = raw |
||||
|
.map((e) => MeetingObjectStorageRecord.fromJson( |
||||
|
Map<String, dynamic>.from(e))) |
||||
|
.toList(); |
||||
|
} |
||||
|
if (uploads.isNotEmpty) { |
||||
|
list.add(MeetingStorageDailyStats( |
||||
|
date: key, userId: getCurrentUserId(), items: uploads)); |
||||
|
} |
||||
|
} |
||||
|
Logger.i('MeetingOSrecord', |
||||
|
'获取会议对象存储历史: start=${_formatDate(startDate)}, end=${_formatDate(endDate)}, 有效天数=${list.length}'); |
||||
|
return list; |
||||
|
} catch (e) { |
||||
|
Logger.error('获取对象存储历史统计失败: $e'); |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
MeetingStorageHistoryStats aggregateMeetingStorageStats( |
||||
|
List<MeetingStorageDailyStats> daily) { |
||||
|
int uploads = 0; |
||||
|
int storageBytes = 0; |
||||
|
int transferBytes = 0; |
||||
|
for (final d in daily) { |
||||
|
uploads += d.storageUploads; |
||||
|
storageBytes += d.storageBytesTotal; |
||||
|
transferBytes += d.transferBytesTotal; |
||||
|
} |
||||
|
Logger.i('MeetingOSrecord', |
||||
|
'聚合对象存储统计: uploads=${uploads}, storageBytes=${storageBytes}, transferBytes=${transferBytes}'); |
||||
|
return MeetingStorageHistoryStats( |
||||
|
userId: getCurrentUserId(), |
||||
|
allStorageUploads: uploads, |
||||
|
allStorageBytes: storageBytes, |
||||
|
allTransferBytes: transferBytes, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import '../../../routes/app_routes.dart'; |
||||
|
|
||||
|
/// 显示充值弹窗 |
||||
|
/// [type] 充值类型,可选值为 'agent'、'translation'、'meeting' |
||||
|
/// 分别跳转对应ai充值、翻译充值、会议充值界面 |
||||
|
void showRechargeDialog(String type) { |
||||
|
Get.dialog( |
||||
|
Dialog( |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(12), |
||||
|
), |
||||
|
child: Stack( |
||||
|
children: [ |
||||
|
Padding( |
||||
|
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
Text( |
||||
|
'tip'.tr, |
||||
|
style: const TextStyle( |
||||
|
fontSize: 18, |
||||
|
fontWeight: FontWeight.bold, |
||||
|
), |
||||
|
), |
||||
|
const SizedBox(height: 12), |
||||
|
Text( |
||||
|
(type == 'agent' |
||||
|
? 'aiMemberExpiredDesc' |
||||
|
: 'insufficientBalanceDesc') |
||||
|
.tr, // 会员过期描述 / 余额不足描述 |
||||
|
textAlign: TextAlign.center, |
||||
|
), |
||||
|
const SizedBox(height: 20), |
||||
|
SizedBox( |
||||
|
width: double.infinity, |
||||
|
child: ElevatedButton( |
||||
|
style: ElevatedButton.styleFrom( |
||||
|
backgroundColor: Colors.orangeAccent, |
||||
|
foregroundColor: Colors.white, |
||||
|
), |
||||
|
onPressed: () { |
||||
|
Get.back(); |
||||
|
if (type == 'agent') { |
||||
|
Get.toNamed(Routes.goodsvip); |
||||
|
} else if (type == 'translation') { |
||||
|
Get.toNamed(Routes.goodstrans); |
||||
|
} else if (type == 'meeting') { |
||||
|
Get.toNamed(Routes.goodsmeet); |
||||
|
} |
||||
|
}, |
||||
|
child: Text('recharge'.tr), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
Positioned( |
||||
|
right: 8, |
||||
|
top: 8, |
||||
|
child: IconButton( |
||||
|
icon: const Icon(Icons.close), |
||||
|
onPressed: () => Get.back(), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
barrierDismissible: true, |
||||
|
); |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import '../controllers/eq_settings_controller.dart'; |
||||
|
|
||||
|
class EqSettingsBinding extends Bindings { |
||||
|
@override |
||||
|
void dependencies() { |
||||
|
Get.lazyPut<EqSettingsController>(() => EqSettingsController()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,217 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:get_storage/get_storage.dart'; |
||||
|
|
||||
|
import '../../../core/utils/logger.dart'; |
||||
|
import '../../../routes/app_routes.dart'; |
||||
|
import '../views/widgets/row_confirm_widget.dart'; |
||||
|
import '../views/widgets/row_equalizer_widget.dart'; |
||||
|
import '../views/widgets/row_grade_widget.dart'; |
||||
|
import '../views/widgets/row_input_widget.dart'; |
||||
|
import '../views/widgets/row_keypress_widget.dart'; |
||||
|
import '../views/widgets/row_radio_widget.dart'; |
||||
|
import '../views/widgets/row_switch_widget.dart'; |
||||
|
import '../views/widgets/row_timer_widget.dart'; |
||||
|
|
||||
|
class EqSettingsController extends GetxController { |
||||
|
final RxList uiList = [ |
||||
|
{ |
||||
|
'title': '音效', |
||||
|
'templates': [ |
||||
|
{ |
||||
|
'type': 'equalizer', |
||||
|
'name': '均衡器', |
||||
|
'value': [], |
||||
|
'options': [ |
||||
|
{ |
||||
|
'label': '默认', |
||||
|
'value': [1, 2, 3, 4, 5, 4, 3, 2, 1, 0], |
||||
|
'default': true, |
||||
|
}, |
||||
|
{ |
||||
|
'label': '流行', |
||||
|
'value': [1, 0, 3, 4, 5, 4, 1, 2, 1, 0], |
||||
|
'default': true, |
||||
|
}, |
||||
|
{ |
||||
|
'label': '摇滚', |
||||
|
'value': [2, 3, 4, 5, 4, 3, 2, 1, 0, -1], |
||||
|
'default': true, |
||||
|
}, |
||||
|
{ |
||||
|
'label': '爵士', |
||||
|
'value': [3, 4, 0, 4, 3, 2, 1, 0, -1, -2], |
||||
|
'default': true, |
||||
|
}, |
||||
|
{ |
||||
|
'label': '经典', |
||||
|
'value': [3, 4, 2, 4, 3, 2, 1, 0, -1, -2], |
||||
|
'default': true, |
||||
|
}, |
||||
|
{ |
||||
|
'label': '乡村', |
||||
|
'value': [3, 4, 1, 4, 3, 2, 1, 0, -1, -2], |
||||
|
'default': true, |
||||
|
}, |
||||
|
], |
||||
|
'xAxis': [ |
||||
|
'8', |
||||
|
'31', |
||||
|
'62', |
||||
|
'125', |
||||
|
'250', |
||||
|
'500', |
||||
|
'1K', |
||||
|
'2K', |
||||
|
'4K', |
||||
|
'16K' |
||||
|
], |
||||
|
'onCallback': (List value) { |
||||
|
Logger.d('equalizer', '均衡器:$value'); |
||||
|
}, |
||||
|
} |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
'title': '按键功能', |
||||
|
'templates': [ |
||||
|
{ |
||||
|
'type': 'keypress', |
||||
|
'data': [ |
||||
|
{'key': 'key1', 'label': '单击', 'left': 1, 'right': 1}, |
||||
|
{'key': 'key2', 'label': '双击', 'left': 1, 'right': 1}, |
||||
|
{'key': 'key3', 'label': '三击', 'left': 1, 'right': 1}, |
||||
|
{'key': 'key4', 'label': '长按', 'left': 1, 'right': 1}, |
||||
|
], |
||||
|
'options': [ |
||||
|
{'label': '无作用', 'value': 0}, |
||||
|
{'label': '回拨电话', 'value': 1}, |
||||
|
{'label': '语音助手', 'value': 2}, |
||||
|
{'label': '上一曲', 'value': 3}, |
||||
|
{'label': '下一曲', 'value': 4}, |
||||
|
{'label': '音量加', 'value': 5}, |
||||
|
{'label': '音量减', 'value': 6}, |
||||
|
{'label': '播放暂停', 'value': 7}, |
||||
|
{'label': '游戏模式', 'value': 8}, |
||||
|
{'label': '降噪设置', 'value': 9}, |
||||
|
{'label': 'EQ切换', 'value': 10}, |
||||
|
], |
||||
|
'onCallback': (Map value) { |
||||
|
Logger.d('keypress', '按键功能:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
'title': '耳机功能', |
||||
|
'templates': [ |
||||
|
{ |
||||
|
'type': 'input', |
||||
|
'name': '修改名称', |
||||
|
'value': 'voitrans', |
||||
|
'onCallback': (String value) { |
||||
|
Logger.d('input', '修改名称:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'radio', |
||||
|
'name': '工作模式', |
||||
|
'value': 1, |
||||
|
'options': [ |
||||
|
{'label': '游戏模式', 'value': 1}, |
||||
|
{'label': '娱乐模式', 'value': 2}, |
||||
|
{'label': '睡眠模式', 'value': 3}, |
||||
|
], |
||||
|
'onCallback': (int value) { |
||||
|
Logger.d('radio', '工作模式:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'switch', |
||||
|
'name': '闪灯开关', |
||||
|
'value': false, |
||||
|
'onCallback': (bool value) { |
||||
|
Logger.d('switch', '闪灯开关:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'switch', |
||||
|
'name': '查找设备', |
||||
|
'value': false, |
||||
|
'description': '在设备上播放声音', |
||||
|
'onCallback': (bool value) { |
||||
|
Logger.d('switch', '查找设备:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'grade', |
||||
|
'name': '降噪等级', |
||||
|
'value': 0, |
||||
|
'onCallback': (int value) { |
||||
|
Logger.d('grade', '降噪等级:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'timer', |
||||
|
'name': '定时关机', |
||||
|
'value': 0, |
||||
|
'onCallback': (int value) { |
||||
|
Logger.d('timer', '定时关机:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
'templates': [ |
||||
|
{ |
||||
|
'type': 'confirm', |
||||
|
'name': '升级耳机固件', |
||||
|
'onCallback': () => Get.toNamed(Routes.OTA_UPGRADE), |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'confirm', |
||||
|
'name': '恢复出厂设置', |
||||
|
'tips': '确定要恢复出厂设置吗?', |
||||
|
'onCallback': () { |
||||
|
Logger.d('confirm', '恢复出厂设置'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'confirm', |
||||
|
'name': '清除配对记录', |
||||
|
'tips': '确定要清除配对记录吗?', |
||||
|
'onCallback': () { |
||||
|
Logger.d('confirm', '清除配对记录'); |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
} |
||||
|
].obs; |
||||
|
|
||||
|
final GetStorage _storage = GetStorage(); |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
final List equalizerOptions = _storage.read('equalizer_options') ?? []; |
||||
|
if (equalizerOptions.isNotEmpty) { |
||||
|
List options = |
||||
|
equalizerOptions.map((e) => Map<String, Object>.from(e)).toList(); |
||||
|
uiList[0]['templates'][0]['options'].addAll(options); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Widget getTemplate(Map data) { |
||||
|
return switch (data['type']) { |
||||
|
'equalizer' => RowEqualizerWidget(data), |
||||
|
'keypress' => RowKeypressWidget(data), |
||||
|
'input' => RowInputWidget(data), |
||||
|
'radio' => RowRadioWidget(data), |
||||
|
'switch' => RowSwitchWidget(data), |
||||
|
'grade' => RowGradeWidget(data), |
||||
|
'timer' => RowTimerWidget(data), |
||||
|
'confirm' => RowConfirmWidget(data), |
||||
|
_ => const SizedBox.shrink(), |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import '../controllers/eq_settings_controller.dart'; |
||||
|
|
||||
|
class EqSettingsView extends GetView<EqSettingsController> { |
||||
|
const EqSettingsView({super.key}); |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return Scaffold( |
||||
|
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], |
||||
|
appBar: _buildAppBar(isDarkMode), |
||||
|
body: _buildBody(isDarkMode), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建应用栏 |
||||
|
PreferredSizeWidget _buildAppBar(bool isDarkMode) { |
||||
|
return AppBar( |
||||
|
backgroundColor: isDarkMode ? Colors.black : Colors.white, |
||||
|
surfaceTintColor: Colors.transparent, |
||||
|
elevation: 0.5, |
||||
|
leading: IconButton( |
||||
|
icon: Icon( |
||||
|
Icons.arrow_back_ios, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
size: 20.sp, |
||||
|
), |
||||
|
onPressed: () => Get.back(), |
||||
|
), |
||||
|
title: Text( |
||||
|
'设备设置', |
||||
|
style: TextStyle( |
||||
|
fontSize: 17.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
centerTitle: true, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建主体内容 |
||||
|
Widget _buildBody(bool isDarkMode) { |
||||
|
return Obx( |
||||
|
() => ListView.builder( |
||||
|
itemCount: controller.uiList.length, |
||||
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), |
||||
|
itemBuilder: (BuildContext context, int index) { |
||||
|
Map data = controller.uiList[index]; |
||||
|
return _item(isDarkMode, data); |
||||
|
}, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _item(bool isDarkMode, Map data) { |
||||
|
return Container( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), |
||||
|
margin: EdgeInsets.symmetric(vertical: 5.w), |
||||
|
decoration: BoxDecoration( |
||||
|
color: isDarkMode ? Colors.white.withValues(alpha: 0.05) : Colors.white, |
||||
|
borderRadius: BorderRadius.circular(8.w), |
||||
|
boxShadow: [ |
||||
|
BoxShadow( |
||||
|
color: isDarkMode |
||||
|
? Colors.black.withValues(alpha: 0.2) |
||||
|
: Colors.black.withValues(alpha: 0.05), |
||||
|
blurRadius: 10, |
||||
|
offset: const Offset(0, 4), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
child: Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
if (data['title'] != null) |
||||
|
Text( |
||||
|
data['title'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
...data['templates'].map((e) => controller.getTemplate(e)).toList(), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue