为了避免再次被说标题党,这里先说明些事情:
第一,android没法直接连接SQLServer,起码我没有发现方法,想想看,sqlserver安装之后有多大,android程序是跑在手机上的,想让程序直接访问sqlserver,那手机要多大的内存?
第二,本文是通过一个“桥梁”——webservice来间接访问SQLServer的,当然还有其他方法,感兴趣的同学可以自行百度。
如果理解了上面两点,好了咱们继续。
教程会拿一个具体的例子来讲,一步一步来,也许细节上还可以继续加工,但大致的流程就是这样的。
本教程有五个部分:
- 项目说明
- 开发环境部署
- 数据库设计
- 服务器端程序设计
- 客户端(android端)程序设计
项目说明
这个项目意在实现一个简单的android连接Sqlserver的功能。
就做一个简单的库存管理功能,包括对仓库内现有货物的查看、货物信息的增加&删除。
开发环境的部署
今天主要讲解第一个部分,开发环境的部署.
操作系统:Windows764bit 旗舰版
当然这个是什么基本无所谓,只是我是在这上面开发的,不过家庭普通版的貌似不能配置IIS,就是咱们后面要使用的一个服务.
android端:eclipse +
ADT集成开发环境
相信看到这个教程的基本都知道如何做这些了.如果真的是有哪位同学android开发环境没有配置好而来看这篇教程,请先移步->www.google.com
服务器端:VisualStudio
2010 旗舰版
这个是用来写website/webservice的,开发语言使用C# (即.net)
数据库:SQLServer2008 R2
其实这个是什么版本也无所谓吧,教程使用的都是比较基本的东西,所以版本的差异基本可以忽略。
IIS
7.5:正确配置并开启IIS服务
如果想将website/webservice发布出去就要开启这个服务。但是如果仅仅是在本地进行测试就不需要配置,直接在VS中运行就可以。
其实我在开发的时候也只是配置IIS的时候遇到了一些问题,这里给出IIS的配置方法.
http://wenku.baidu.com/view/95cf9fd9ad51f01dc281f1af.html这篇文库给的还是很详细的,我当初就是照着这个配置的。
数据库设计
数据库名称:StockManage
表设计
表名称:C
表说明:
列名 |
中文名称 |
数据型态 |
必填 |
说明 |
Cno |
货物编号 |
Int |
V |
主键,自增 |
Cname |
货物名称 |
String |
|
|
Cnum |
货物数量 |
Int |
|
|
下图是设计表的时候的截图。
向表中输入内容
吐槽一下:为什么这里猫、狗、电话都有,甚至还有Surface?!这只能说当时LZ在想这些……
服务器端程序设计(Webservice)
其实服务端可以写成webservice也可以写成website,前者只是提供一种服务,而后者是可以提供用户界面等具体的页面,后者也就是咱们平时所说的“网站”。
两者的区别:
- Web Service 只提供程序和接口,不提供用户界面
- Web Site 提供程序和接口,也提供用户界面(网页)
由于咱们只是需要一个中介来访问sqlserver,所以写成webservice足够了。
目标:写一个Website访问Sqlserver,获取数据并转换成xml格式,然后传递给android客户端。
1. 新建一个Webservice工程
2. 视图 -> 其它窗口 -> 服务器资源管理器
3. 右键数据连接 -> 添加连接
4. 选择Microsoft Sqlserver
5. 如下图所示选择(可以点击测试连接来检测连接是否成功,然后点击确定)
6. 数据库的查看和编辑也可以在VS中进行了
7. 先查看一下数据库属性并记录下连接属性
8. 新建一个类DBOperation,代码如下:
9. 修改Service1.asmx.cs代码如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Web;
- using System.Web.Services;
-
- namespace StockManageWebservice
- {
-
-
-
- [WebService(Namespace = "http://tempuri.org/")]
- [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
- [System.ComponentModel.ToolboxItem(false)]
-
-
- public class Service1 : System.Web.Services.WebService
- {
- DBOperation dbOperation = new DBOperation();
-
- [WebMethod]
- public string HelloWorld()
- {
- return "Hello World";
- }
-
- [WebMethod(Description = "获取所有货物的信息")]
- public string[] selectAllCargoInfor()
- {
- return dbOperation.selectAllCargoInfor().ToArray();
- }
-
- [WebMethod(Description = "增加一条货物信息")]
- public bool insertCargoInfo(string Cname, int Cnum)
- {
- return dbOperation.insertCargoInfo(Cname, Cnum);
- }
-
- [WebMethod(Description = "删除一条货物信息")]
- public bool deleteCargoInfo(string Cno)
- {
- return dbOperation.deleteCargoInfo(Cno);
- }
- }
- }
10. 运行程序(F5),会自动打开一个浏览器,可以看到如下画面:
11. 选择相应的功能并传递参数可以实现调试从浏览器中调试程序:
下图选择的是增加一条货物信息
12. 程序执行的结果:
13.另,记住这里的端口名,后面android的程序中添入的端口号就是这个:
客户端(android端)程序设计
程序代码:
1.MainActivity
- package com.bottle.stockmanage;
-
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
-
- import android.app.Activity;
- import android.app.Dialog;
- import android.os.Bundle;
- import android.view.Gravity;
- import android.view.View;
- import android.view.View.OnClickListener;
- import android.view.Window;
- import android.view.WindowManager;
- import android.widget.Button;
- import android.widget.EditText;
- import android.widget.ListView;
- import android.widget.SimpleAdapter;
- import android.widget.Toast;
-
- public class MainActivity extends Activity{
-
- private Button btn1;
- private Button btn2;
- private Button btn3;
- private ListView listView;
- private SimpleAdapter adapter;
- private DBUtil dbUtil;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- btn1 = (Button) findViewById(R.id.btn_all);
- btn2 = (Button) findViewById(R.id.btn_add);
- btn3 = (Button) findViewById(R.id.btn_delete);
- listView = (ListView) findViewById(R.id.listView);
- dbUtil = new DBUtil();
-
- btn1.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- hideButton(true);
- setListView();
- }
- });
-
- btn2.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- hideButton(true);
- setAddDialog();
- }
- });
-
- btn3.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- hideButton(true);
- setDeleteDialog();
- }
- });
- }
-
-
-
-
- private void setDeleteDialog() {
-
- final Dialog dialog = new Dialog(MainActivity.this);
- dialog.setContentView(R.layout.dialog_delete);
- dialog.setTitle("输入想要删除的货物的编号");
- Window dialogWindow = dialog.getWindow();
- WindowManager.LayoutParams lp = dialogWindow.getAttributes();
- dialogWindow.setGravity(Gravity.CENTER);
- dialogWindow.setAttributes(lp);
-
- final EditText cNoEditText = (EditText) dialog.findViewById(R.id.editText1);
- Button btnConfirm = (Button) dialog.findViewById(R.id.button1);
- Button btnCancel = (Button) dialog.findViewById(R.id.button2);
-
- btnConfirm.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- dbUtil.deleteCargoInfo(cNoEditText.getText().toString());
- dialog.dismiss();
- hideButton(false);
- Toast.makeText(MainActivity.this, "成功删除数据", Toast.LENGTH_SHORT).show();
- }
- });
-
- btnCancel.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- dialog.dismiss();
- hideButton(false);
- }
- });
-
- dialog.show();
- }
-
-
-
-
- private void setAddDialog() {
-
- final Dialog dialog = new Dialog(MainActivity.this);
- dialog.setContentView(R.layout.dialog_add);
- dialog.setTitle("输入添加的货物的信息");
- Window dialogWindow = dialog.getWindow();
- WindowManager.LayoutParams lp = dialogWindow.getAttributes();
- dialogWindow.setGravity(Gravity.CENTER);
- dialogWindow.setAttributes(lp);
-
- final EditText cNameEditText = (EditText) dialog.findViewById(R.id.editText1);
- final EditText cNumEditText = (EditText) dialog.findViewById(R.id.editText2);
- Button btnConfirm = (Button) dialog.findViewById(R.id.button1);
- Button btnCancel = (Button) dialog.findViewById(R.id.button2);
-
- btnConfirm.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
-
- dbUtil.insertCargoInfo(cNameEditText.getText().toString(), cNumEditText.getText().toString());
- dialog.dismiss();
- hideButton(false);
- Toast.makeText(MainActivity.this, "成功添加数据", Toast.LENGTH_SHORT).show();
- }
- });
-
- btnCancel.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- dialog.dismiss();
- hideButton(false);
- }
- });
- dialog.show();
- }
-
-
-
-
- private void setListView() {
-
- listView.setVisibility(View.VISIBLE);
-
- List<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
-
- list = dbUtil.getAllInfo();
-
- adapter = new SimpleAdapter(
- MainActivity.this,
- list,
- R.layout.adapter_item,
- new String[] { "Cno", "Cname", "Cnum" },
- new int[] { R.id.txt_Cno, R.id.txt_Cname, R.id.txt_Cnum });
-
- listView.setAdapter(adapter);
-
- }
-
-
-
-
- private void hideButton(boolean result) {
- if (result) {
- btn1.setVisibility(View.GONE);
- btn2.setVisibility(View.GONE);
- btn3.setVisibility(View.GONE);
- } else {
- btn1.setVisibility(View.VISIBLE);
- btn2.setVisibility(View.VISIBLE);
- btn3.setVisibility(View.VISIBLE);
- }
-
- }
-
-
-
-
- @Override
- public void onBackPressed()
- {
- if (listView.getVisibility() == View.VISIBLE) {
- listView.setVisibility(View.GONE);
- hideButton(false);
- }else {
- MainActivity.this.finish();
- }
- }
- }
2.HttpConnSoap
(改类已经过时,更多请参照
http://blog.csdn.net/zhyl8157121/article/details/8709048)
- package com.bottle.stockmanage;
-
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.net.HttpURLConnection;
- import java.net.URL;
- import java.util.ArrayList;
-
- public class HttpConnSoap {
- public ArrayList<String> GetWebServre(String methodName, ArrayList<String> Parameters, ArrayList<String> ParValues) {
- ArrayList<String> Values = new ArrayList<String>();
-
-
-
-
-
- String ServerUrl = "http://10.0.2.2:11125/Service1.asmx";
-
-
- String soapAction = "http://tempuri.org/" + methodName;
-
- String soap = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
- + "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
- + "<soap:Body />";
- String tps, vps, ts;
- String mreakString = "";
-
- mreakString = "<" + methodName + " xmlns=\"http://tempuri.org/\">";
- for (int i = 0; i < Parameters.size(); i++) {
- tps = Parameters.get(i).toString();
-
- vps = ParValues.get(i).toString();
- ts = "<" + tps + ">" + vps + "</" + tps + ">";
- mreakString = mreakString + ts;
- }
- mreakString = mreakString + "</" + methodName + ">";
-
-
-
-
-
-
- String soap2 = "</soap:Envelope>";
- String requestData = soap + mreakString + soap2;
-
-
- try {
- URL url = new URL(ServerUrl);
- HttpURLConnection con = (HttpURLConnection) url.openConnection();
- byte[] bytes = requestData.getBytes("utf-8");
- con.setDoInput(true);
- con.setDoOutput(true);
- con.setUseCaches(false);
- con.setConnectTimeout(6000);
- con.setRequestMethod("POST");
- con.setRequestProperty("Content-Type", "text/xml;charset=utf-8");
- con.setRequestProperty("SOAPAction", soapAction);
- con.setRequestProperty("Content-Length", "" + bytes.length);
- OutputStream outStream = con.getOutputStream();
- outStream.write(bytes);
- outStream.flush();
- outStream.close();
- InputStream inStream = con.getInputStream();
-
-
-
- Values = inputStreamtovaluelist(inStream, methodName);
-
- return Values;
-
- } catch (Exception e) {
- System.out.print("2221");
- return null;
- }
- }
-
- public ArrayList<String> inputStreamtovaluelist(InputStream in, String MonthsName) throws IOException {
- StringBuffer out = new StringBuffer();
- String s1 = "";
- byte[] b = new byte[4096];
- ArrayList<String> Values = new ArrayList<String>();
- Values.clear();
-
- for (int n; (n = in.read(b)) != -1;) {
- s1 = new String(b, 0, n);
- out.append(s1);
- }
-
- System.out.println(out);
- String[] s13 = s1.split("><");
- String ifString = MonthsName + "Result";
- String TS = "";
- String vs = "";
-
- Boolean getValueBoolean = false;
- for (int i = 0; i < s13.length; i++) {
- TS = s13[i];
- System.out.println(TS);
- int j, k, l;
- j = TS.indexOf(ifString);
- k = TS.lastIndexOf(ifString);
-
- if (j >= 0) {
- System.out.println(j);
- if (getValueBoolean == false) {
- getValueBoolean = true;
- } else {
-
- }
-
- if ((j >= 0) && (k > j)) {
- System.out.println("FFF" + TS.lastIndexOf("/" + ifString));
-
- l = ifString.length() + 1;
- vs = TS.substring(j + l, k - 2);
-
- Values.add(vs);
- System.out.println("退出" + vs);
- getValueBoolean = false;
- return Values;
- }
-
- }
- if (TS.lastIndexOf("/" + ifString) >= 0) {
- getValueBoolean = false;
- return Values;
- }
- if ((getValueBoolean) && (TS.lastIndexOf("/" + ifString) < 0) && (j < 0)) {
- k = TS.length();
-
- vs = TS.substring(7, k - 8);
-
- Values.add(vs);
- }
-
- }
-
- return Values;
- }
-
- }
3.DBUtil
- package com.bottle.stockmanage;
-
- import java.sql.Connection;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
-
- public class DBUtil {
- private ArrayList<String> arrayList = new ArrayList<String>();
- private ArrayList<String> brrayList = new ArrayList<String>();
- private ArrayList<String> crrayList = new ArrayList<String>();
- private HttpConnSoap Soap = new HttpConnSoap();
-
- public static Connection getConnection() {
- Connection con = null;
- try {
-
-
- } catch (Exception e) {
-
- }
- return con;
- }
-
-
-
-
-
-
- public List<HashMap<String, String>> getAllInfo() {
- List<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
-
- arrayList.clear();
- brrayList.clear();
- crrayList.clear();
-
- crrayList = Soap.GetWebServre("selectAllCargoInfor", arrayList, brrayList);
-
- HashMap<String, String> tempHash = new HashMap<String, String>();
- tempHash.put("Cno", "Cno");
- tempHash.put("Cname", "Cname");
- tempHash.put("Cnum", "Cnum");
- list.add(tempHash);
-
- for (int j = 0; j < crrayList.size(); j += 3) {
- HashMap<String, String> hashMap = new HashMap<String, String>();
- hashMap.put("Cno", crrayList.get(j));
- hashMap.put("Cname", crrayList.get(j + 1));
- hashMap.put("Cnum", crrayList.get(j + 2));
- list.add(hashMap);
- }
-
- return list;
- }
-
-
-
-
-
-
- public void insertCargoInfo(String Cname, String Cnum) {
-
- arrayList.clear();
- brrayList.clear();
-
- arrayList.add("Cname");
- arrayList.add("Cnum");
- brrayList.add(Cname);
- brrayList.add(Cnum);
-
- Soap.GetWebServre("insertCargoInfo", arrayList, brrayList);
- }
-
-
-
-
-
-
- public void deleteCargoInfo(String Cno) {
-
- arrayList.clear();
- brrayList.clear();
-
- arrayList.add("Cno");
- brrayList.add(Cno);
-
- Soap.GetWebServre("deleteCargoInfo", arrayList, brrayList);
- }
- }
4.activity_main.xml
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" >
-
- <ListView
- android:id="@+id/listView"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:visibility="gone" >
- </ListView>
-
- <Button
- android:id="@+id/btn_all"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_above="@+id/btn_add"
- android:layout_alignLeft="@+id/btn_add"
- android:layout_marginBottom="10dip"
- android:text="@string/btn1" />
-
- <Button
- android:id="@+id/btn_add"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:layout_centerVertical="true"
- android:text="@string/btn2" />
-
- <Button
- android:id="@+id/btn_delete"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignLeft="@+id/btn_add"
- android:layout_below="@+id/btn_add"
- android:layout_marginTop="10dip"
- android:text="@string/btn3" />
-
- </RelativeLayout>
5.adapter_item.xml
- <?xml version="1.0" encoding="utf-8"?>
- <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:descendantFocusability="blocksDescendants"
- android:gravity="center" >
-
- <TableRow
- android:id="@+id/classroom_detail_item_tableRow"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center" >
-
- <TextView
- android:id="@+id/txt_Cno"
- android:layout_width="80dp"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:height="40dp"
- android:textSize="14sp" >
- </TextView>
-
- <TextView
- android:id="@+id/txt_Cname"
- android:layout_width="80dp"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:height="40dp"
- android:textSize="14sp" >
- </TextView>
-
- <TextView
- android:id="@+id/txt_Cnum"
- android:layout_width="80dp"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:height="40dp"
- android:textSize="14sp" >
- </TextView>
-
- </TableRow>
-
- </TableLayout>
6.dialog_add.xml
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="vertical" >
-
- <EditText
- android:id="@+id/editText1"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:ems="10"
- android:hint="@string/add_hint1" >
-
- <requestFocus />
- </EditText>
-
- <EditText
- android:id="@+id/editText2"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:ems="10"
- android:hint="@string/add_hint2"
- android:inputType="number" />
-
- <LinearLayout
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal" >
-
- <Button
- android:id="@+id/button1"
- android:layout_width="100dip"
- android:layout_height="wrap_content"
- android:layout_marginLeft="20dip"
- android:text="@string/confirm" />
-
- <Button
- android:id="@+id/button2"
- android:layout_width="100dip"
- android:layout_height="wrap_content"
- android:layout_marginLeft="40dip"
- android:text="@string/cancel" />
- </LinearLayout>
-
- </LinearLayout>
7.dialog_delete.xml
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="vertical" >
-
- <EditText
- android:id="@+id/editText1"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:ems="10"
- android:hint="@string/delete_hint" >
-
- <requestFocus />
- </EditText>
-
- <LinearLayout
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal" >
-
- <Button
- android:id="@+id/button1"
- android:layout_width="100dip"
- android:layout_height="wrap_content"
- android:layout_marginLeft="20dip"
- android:text="@string/confirm" />
-
- <Button
- android:id="@+id/button2"
- android:layout_width="100dip"
- android:layout_height="wrap_content"
- android:layout_marginLeft="40dip"
- android:text="@string/cancel" />
- </LinearLayout>
-
- </LinearLayout>
8.strings.xml
- <resources>
-
- <string name="app_name">StockManagement</string>
- <string name="menu_settings">Settings</string>
- <string name="title_activity_main">MainActivity</string>
- <string name="btn1">查看所有货物信息</string>
- <string name="btn2">增加一条货物信息</string>
- <string name="btn3">删除一条货物信息</string>
- <string name="add_hint1">输入添加的货物的名称</string>
- <string name="add_hint2">输入货物的数量</string>
- <string name="confirm">确定</string>
- <string name="cancel">取消</string>
- <string name="delete_hint">输入删除的货物的编号</string>
-
- </resources>
9.Manifest.xml
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.bottle.stockmanage"
- android:versionCode="1"
- android:versionName="1.0" >
-
- <uses-sdk
- android:minSdkVersion="7"
- android:targetSdkVersion="15" />
-
- <uses-permission android:name="android.permission.INTERNET" />
-
- <application
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:theme="@android:style/Theme.NoTitleBar" >
- <activity
- android:name=".MainActivity"
- android:label="@string/title_activity_main"
- android:screenOrientation="portrait" >
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
-
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- </application>
-
- </manifest>
运行程序的效果如下图所示:
再说一下IIS,如果只是在本地进行测试等操作,是不需要使用到IIS的,但是如果想发布出去,就要配置一下IIS。
好啦,基本就是这样了。程序不是完善的,但大概的思路就是这样,用到的技术也大概就是这几样,但是每一样拿出来都够学一阵的了。
--->2012.12.02 增加内容
附上本文demo的CSDN下载地址
http://download.csdn.net/detail/zhyl8157121/4836107
--->2013.01.08 增加内容
解释一下android端如何和webservice通信的。(如何修改实例程序)
具体的更深层的东西已经在HttpSoap中封装好了,所以大家使用的时候可以直接用这个类就可以了。(我也不懂是怎么实现的……)
android调用的方法就是如DBUtil中那样,比如
其中arrayList中和brrayList中分别存放对应的webservice中“selectAllCargoInfor”方法的参数名和参数的值。
由于webservice中的selectAllCargoInfo方法的参数为空,所以对应的,android端调用的时候,arrayList和brrayList的值就是空的。
所以大家在使用的时候,只需要将webservice中的方法写好,然后写好DBUtil中的调用参数即可。
--->2013.03.23 增加内容
如果获取值为空,可能是返回值是复杂类型造成的,可以参考:http://blog.csdn.net/zhyl8157121/article/details/8709048
谢谢支持,欢迎大家批评指正。
Android通过webservice连接SQLServer 详细教程(数据库+服务器+客户端),布布扣,bubuko.com
Android通过webservice连接SQLServer 详细教程(数据库+服务器+客户端)
原文:http://www.cnblogs.com/ywsoftware/p/3754575.html